diff options
Diffstat (limited to 'ansible_collections/community/windows')
668 files changed, 74999 insertions, 0 deletions
diff --git a/ansible_collections/community/windows/.azure-pipelines/azure-pipelines.yml b/ansible_collections/community/windows/.azure-pipelines/azure-pipelines.yml new file mode 100644 index 000000000..2b5d2e8b0 --- /dev/null +++ b/ansible_collections/community/windows/.azure-pipelines/azure-pipelines.yml @@ -0,0 +1,166 @@ +trigger: + batch: true + branches: + include: + - main + - stable-* + +pr: + autoCancel: true + branches: + include: + - main + - stable-* + +schedules: + - cron: 0 9 * * * + displayName: Nightly + always: true + branches: + include: + - main + - stable-* + +variables: + - name: checkoutPath + value: ansible_collections/community/windows + - name: coverageBranches + value: main + - name: pipelinesCoverage + value: coverage-powershell + - name: entryPoint + value: tests/utils/shippable/shippable.sh + - name: fetchDepth + value: 0 + +resources: + containers: + - container: default + image: quay.io/ansible/azure-pipelines-test-container:3.0.0 + +pool: Standard + +stages: + - stage: Dependencies + displayName: Dependencies + jobs: + - job: dep_download + displayName: Download Dependencies + pool: + vmImage: ubuntu-latest + steps: + - checkout: self + fetchDepth: 1 + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.10' + - bash: python -m pip install ansible-core + displayName: Install Ansible + - bash: ansible-galaxy collection install -r tests/requirements.yml -p collections + displayName: Install collection requirements + - task: PublishPipelineArtifact@1 + inputs: + targetPath: collections + artifactName: CollectionRequirements + - stage: Ansible_devel + displayName: Ansible devel + dependsOn: + - Dependencies + jobs: + - template: templates/matrix.yml + parameters: + nameFormat: '{0}' + testFormat: 'devel/{0}' + targets: + - name: Sanity + test: sanity + - name: Units + test: units + - stage: Ansible_2_15 + displayName: Ansible 2.15 + dependsOn: + - Dependencies + jobs: + - template: templates/matrix.yml + parameters: + nameFormat: '{0}' + testFormat: '2.15/{0}' + targets: + - name: Sanity + test: sanity + - name: Units + test: units + - stage: Ansible_2_14 + displayName: Ansible 2.14 + dependsOn: + - Dependencies + jobs: + - template: templates/matrix.yml + parameters: + nameFormat: '{0}' + testFormat: '2.14/{0}' + targets: + - name: Sanity + test: sanity + - name: Units + test: units + - stage: Ansible_2_13 + displayName: Ansible 2.13 + dependsOn: + - Dependencies + jobs: + - template: templates/matrix.yml + parameters: + nameFormat: '{0}' + testFormat: '2.13/{0}' + targets: + - name: Sanity + test: sanity + - name: Units + test: units + - stage: Ansible_2_12 + displayName: Ansible 2.12 + dependsOn: + - Dependencies + jobs: + - template: templates/matrix.yml + parameters: + nameFormat: '{0}' + testFormat: '2.12/{0}' + targets: + - name: Sanity + test: sanity + - name: Units + test: units + - stage: Windows + displayName: Windows + dependsOn: + - Dependencies + jobs: + - template: templates/matrix.yml + parameters: + nameFormat: Server {0} + testFormat: devel/windows/{0} + targets: + - test: 2012 + - test: 2012-R2 + - test: 2016 + - test: 2019 + - test: 2022 + groups: + - 1 + - 2 + - 3 + - 4 + - 5 + - stage: Summary + condition: succeededOrFailed() + dependsOn: + - Ansible_devel + - Ansible_2_15 + - Ansible_2_14 + - Ansible_2_13 + - Ansible_2_12 + - Windows + jobs: + - template: templates/coverage.yml diff --git a/ansible_collections/community/windows/.azure-pipelines/scripts/aggregate-coverage.sh b/ansible_collections/community/windows/.azure-pipelines/scripts/aggregate-coverage.sh new file mode 100755 index 000000000..f3113dd0a --- /dev/null +++ b/ansible_collections/community/windows/.azure-pipelines/scripts/aggregate-coverage.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Aggregate code coverage results for later processing. + +set -o pipefail -eu + +agent_temp_directory="$1" + +PATH="${PWD}/bin:${PATH}" + +mkdir "${agent_temp_directory}/coverage/" + +options=(--venv --venv-system-site-packages --color -v) + +ansible-test coverage combine --export "${agent_temp_directory}/coverage/" "${options[@]}" + +if ansible-test coverage analyze targets generate --help >/dev/null 2>&1; then + # Only analyze coverage if the installed version of ansible-test supports it. + # Doing so allows this script to work unmodified for multiple Ansible versions. + ansible-test coverage analyze targets generate "${agent_temp_directory}/coverage/coverage-analyze-targets.json" "${options[@]}" +fi diff --git a/ansible_collections/community/windows/.azure-pipelines/scripts/combine-coverage.py b/ansible_collections/community/windows/.azure-pipelines/scripts/combine-coverage.py new file mode 100755 index 000000000..506ade646 --- /dev/null +++ b/ansible_collections/community/windows/.azure-pipelines/scripts/combine-coverage.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +""" +Combine coverage data from multiple jobs, keeping the data only from the most recent attempt from each job. +Coverage artifacts must be named using the format: "Coverage $(System.JobAttempt) {StableUniqueNameForEachJob}" +The recommended coverage artifact name format is: Coverage $(System.JobAttempt) $(System.StageDisplayName) $(System.JobDisplayName) +Keep in mind that Azure Pipelines does not enforce unique job display names (only names). +It is up to pipeline authors to avoid name collisions when deviating from the recommended format. +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import re +import shutil +import sys + + +def main(): + """Main program entry point.""" + source_directory = sys.argv[1] + + if '/ansible_collections/' in os.getcwd(): + output_path = "tests/output" + else: + output_path = "test/results" + + destination_directory = os.path.join(output_path, 'coverage') + + if not os.path.exists(destination_directory): + os.makedirs(destination_directory) + + jobs = {} + count = 0 + + for name in os.listdir(source_directory): + match = re.search('^Coverage (?P<attempt>[0-9]+) (?P<label>.+)$', name) + label = match.group('label') + attempt = int(match.group('attempt')) + jobs[label] = max(attempt, jobs.get(label, 0)) + + for label, attempt in jobs.items(): + name = 'Coverage {attempt} {label}'.format(label=label, attempt=attempt) + source = os.path.join(source_directory, name) + source_files = os.listdir(source) + + for source_file in source_files: + source_path = os.path.join(source, source_file) + destination_path = os.path.join(destination_directory, source_file + '.' + label) + print('"%s" -> "%s"' % (source_path, destination_path)) + shutil.copyfile(source_path, destination_path) + count += 1 + + print('Coverage file count: %d' % count) + print('##vso[task.setVariable variable=coverageFileCount]%d' % count) + print('##vso[task.setVariable variable=outputPath]%s' % output_path) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/windows/.azure-pipelines/scripts/process-results.sh b/ansible_collections/community/windows/.azure-pipelines/scripts/process-results.sh new file mode 100755 index 000000000..f3f1d1bae --- /dev/null +++ b/ansible_collections/community/windows/.azure-pipelines/scripts/process-results.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Check the test results and set variables for use in later steps. + +set -o pipefail -eu + +if [[ "$PWD" =~ /ansible_collections/ ]]; then + output_path="tests/output" +else + output_path="test/results" +fi + +echo "##vso[task.setVariable variable=outputPath]${output_path}" + +if compgen -G "${output_path}"'/junit/*.xml' > /dev/null; then + echo "##vso[task.setVariable variable=haveTestResults]true" +fi + +if compgen -G "${output_path}"'/bot/ansible-test-*' > /dev/null; then + echo "##vso[task.setVariable variable=haveBotResults]true" +fi + +if compgen -G "${output_path}"'/coverage/*' > /dev/null; then + echo "##vso[task.setVariable variable=haveCoverageData]true" +fi diff --git a/ansible_collections/community/windows/.azure-pipelines/scripts/publish-codecov.sh b/ansible_collections/community/windows/.azure-pipelines/scripts/publish-codecov.sh new file mode 100755 index 000000000..6d184f0b8 --- /dev/null +++ b/ansible_collections/community/windows/.azure-pipelines/scripts/publish-codecov.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Upload code coverage reports to codecov.io. +# Multiple coverage files from multiple languages are accepted and aggregated after upload. +# Python coverage, as well as PowerShell and Python stubs can all be uploaded. + +set -o pipefail -eu + +output_path="$1" + +curl --silent --show-error https://ansible-ci-files.s3.us-east-1.amazonaws.com/codecov/codecov.sh > codecov.sh + +for file in "${output_path}"/reports/coverage*.xml; do + name="${file}" + name="${name##*/}" # remove path + name="${name##coverage=}" # remove 'coverage=' prefix if present + name="${name%.xml}" # remove '.xml' suffix + + bash codecov.sh \ + -f "${file}" \ + -n "${name}" \ + -X coveragepy \ + -X gcov \ + -X fix \ + -X search \ + -X xcode \ + || echo "Failed to upload code coverage report to codecov.io: ${file}" +done diff --git a/ansible_collections/community/windows/.azure-pipelines/scripts/report-coverage.sh b/ansible_collections/community/windows/.azure-pipelines/scripts/report-coverage.sh new file mode 100755 index 000000000..050464be3 --- /dev/null +++ b/ansible_collections/community/windows/.azure-pipelines/scripts/report-coverage.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Generate code coverage reports for uploading to Azure Pipelines and codecov.io. + +set -o pipefail -eu + +PATH="${PWD}/bin:${PATH}" + +if ! ansible-test --help >/dev/null 2>&1; then + # Install the devel version of ansible-test for generating code coverage reports. + # This is only used by Ansible Collections, which are typically tested against multiple Ansible versions (in separate jobs). + # Since a version of ansible-test is required that can work the output from multiple older releases, the devel version is used. + pip install https://github.com/ansible/ansible/archive/devel.tar.gz --disable-pip-version-check +fi + +# Generate stubs using docker (if supported) otherwise fall back to using a virtual environment instead. +# The use of docker is required when Powershell code is present, but Ansible 2.12 was the first version to support --docker with coverage. +ansible-test coverage xml --stub --docker --color -v || ansible-test coverage xml --stub --venv --venv-system-site-packages --color -v diff --git a/ansible_collections/community/windows/.azure-pipelines/scripts/run-tests.sh b/ansible_collections/community/windows/.azure-pipelines/scripts/run-tests.sh new file mode 100755 index 000000000..a947fdf01 --- /dev/null +++ b/ansible_collections/community/windows/.azure-pipelines/scripts/run-tests.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Configure the test environment and run the tests. + +set -o pipefail -eu + +entry_point="$1" +test="$2" +read -r -a coverage_branches <<< "$3" # space separated list of branches to run code coverage on for scheduled builds + +export COMMIT_MESSAGE +export COMPLETE +export COVERAGE +export IS_PULL_REQUEST + +if [ "${SYSTEM_PULLREQUEST_TARGETBRANCH:-}" ]; then + IS_PULL_REQUEST=true + COMMIT_MESSAGE=$(git log --format=%B -n 1 HEAD^2) +else + IS_PULL_REQUEST= + COMMIT_MESSAGE=$(git log --format=%B -n 1 HEAD) +fi + +COMPLETE= +COVERAGE= + +if [ "${BUILD_REASON}" = "Schedule" ]; then + COMPLETE=yes + + if printf '%s\n' "${coverage_branches[@]}" | grep -q "^${BUILD_SOURCEBRANCHNAME}$"; then + COVERAGE=yes + fi +fi + +"${entry_point}" "${test}" 2>&1 | "$(dirname "$0")/time-command.py" diff --git a/ansible_collections/community/windows/.azure-pipelines/scripts/time-command.py b/ansible_collections/community/windows/.azure-pipelines/scripts/time-command.py new file mode 100755 index 000000000..5e8eb8d4c --- /dev/null +++ b/ansible_collections/community/windows/.azure-pipelines/scripts/time-command.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +"""Prepends a relative timestamp to each input line from stdin and writes it to stdout.""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import sys +import time + + +def main(): + """Main program entry point.""" + start = time.time() + + sys.stdin.reconfigure(errors='surrogateescape') + sys.stdout.reconfigure(errors='surrogateescape') + + for line in sys.stdin: + seconds = time.time() - start + sys.stdout.write('%02d:%02d %s' % (seconds // 60, seconds % 60, line)) + sys.stdout.flush() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/windows/.azure-pipelines/templates/coverage.yml b/ansible_collections/community/windows/.azure-pipelines/templates/coverage.yml new file mode 100644 index 000000000..4d381c6d6 --- /dev/null +++ b/ansible_collections/community/windows/.azure-pipelines/templates/coverage.yml @@ -0,0 +1,42 @@ +# This template adds a job for processing code coverage data. +# It will upload results to Azure Pipelines and codecov.io. +# Use it from a job stage that completes after all other jobs have completed. +# This can be done by placing it in a separate summary stage that runs after the test stage(s) have completed. + +jobs: + - job: Coverage + displayName: Code Coverage + container: default + workspace: + clean: all + steps: + - checkout: self + fetchDepth: $(fetchDepth) + path: $(checkoutPath) + - task: DownloadPipelineArtifact@2 + displayName: Download Coverage Data + inputs: + path: coverage/ + patterns: "Coverage */*=coverage.combined" + - bash: .azure-pipelines/scripts/combine-coverage.py coverage/ + displayName: Combine Coverage Data + - bash: .azure-pipelines/scripts/report-coverage.sh + displayName: Generate Coverage Report + condition: gt(variables.coverageFileCount, 0) + - task: PublishCodeCoverageResults@1 + inputs: + codeCoverageTool: Cobertura + # Azure Pipelines only accepts a single coverage data file. + # That means only Python or PowerShell coverage can be uploaded, but not both. + # Set the "pipelinesCoverage" variable to determine which type is uploaded. + # Use "coverage" for Python and "coverage-powershell" for PowerShell. + summaryFileLocation: "$(outputPath)/reports/$(pipelinesCoverage).xml" + # Override the root (sources) path specified in the coverage XML files. + # This allows coverage to be reported in Azure Pipelines even if the report was generated in a container. + pathToSources: "$(Agent.BuildDirectory)/$(checkoutPath)" + displayName: Publish to Azure Pipelines + condition: gt(variables.coverageFileCount, 0) + - bash: .azure-pipelines/scripts/publish-codecov.sh "$(outputPath)" + displayName: Publish to codecov.io + condition: gt(variables.coverageFileCount, 0) + continueOnError: true diff --git a/ansible_collections/community/windows/.azure-pipelines/templates/matrix.yml b/ansible_collections/community/windows/.azure-pipelines/templates/matrix.yml new file mode 100644 index 000000000..4e9555dd3 --- /dev/null +++ b/ansible_collections/community/windows/.azure-pipelines/templates/matrix.yml @@ -0,0 +1,55 @@ +# This template uses the provided targets and optional groups to generate a matrix which is then passed to the test template. +# If this matrix template does not provide the required functionality, consider using the test template directly instead. + +parameters: + # A required list of dictionaries, one per test target. + # Each item in the list must contain a "test" or "name" key. + # Both may be provided. If one is omitted, the other will be used. + - name: targets + type: object + + # An optional list of values which will be used to multiply the targets list into a matrix. + # Values can be strings or numbers. + - name: groups + type: object + default: [] + + # An optional format string used to generate the job name. + # - {0} is the name of an item in the targets list. + - name: nameFormat + type: string + default: "{0}" + + # An optional format string used to generate the test name. + # - {0} is the name of an item in the targets list. + - name: testFormat + type: string + default: "{0}" + + # An optional format string used to add the group to the job name. + # {0} is the formatted name of an item in the targets list. + # {{1}} is the group -- be sure to include the double "{{" and "}}". + - name: nameGroupFormat + type: string + default: "{0} - {{1}}" + + # An optional format string used to add the group to the test name. + # {0} is the formatted test of an item in the targets list. + # {{1}} is the group -- be sure to include the double "{{" and "}}". + - name: testGroupFormat + type: string + default: "{0}/{{1}}" + +jobs: + - template: test.yml + parameters: + jobs: + - ${{ if eq(length(parameters.groups), 0) }}: + - ${{ each target in parameters.targets }}: + - name: ${{ format(parameters.nameFormat, coalesce(target.name, target.test)) }} + test: ${{ format(parameters.testFormat, coalesce(target.test, target.name)) }} + - ${{ if not(eq(length(parameters.groups), 0)) }}: + - ${{ each group in parameters.groups }}: + - ${{ each target in parameters.targets }}: + - name: ${{ format(format(parameters.nameGroupFormat, parameters.nameFormat), coalesce(target.name, target.test), group) }} + test: ${{ format(format(parameters.testGroupFormat, parameters.testFormat), coalesce(target.test, target.name), group) }} diff --git a/ansible_collections/community/windows/.azure-pipelines/templates/test.yml b/ansible_collections/community/windows/.azure-pipelines/templates/test.yml new file mode 100644 index 000000000..4f859c981 --- /dev/null +++ b/ansible_collections/community/windows/.azure-pipelines/templates/test.yml @@ -0,0 +1,53 @@ +# This template uses the provided list of jobs to create test one or more test jobs. +# It can be used directly if needed, or through the matrix template. + +parameters: + # A required list of dictionaries, one per test job. + # Each item in the list must contain a "job" and "name" key. + - name: jobs + type: object + +jobs: + - ${{ each job in parameters.jobs }}: + - job: test_${{ replace(replace(replace(replace(job.test, '/', '_'), '.', '_'), '-', '_'), '@', '_') }} + displayName: ${{ job.name }} + container: default + workspace: + clean: all + steps: + - checkout: self + fetchDepth: $(fetchDepth) + path: $(checkoutPath) + - task: DownloadPipelineArtifact@2 + inputs: + artifact: CollectionRequirements + path: $(Agent.TempDirectory) + - bash: | + sudo chown -R "$(whoami)" "${PWD}/../../../ansible_collections" + cp -R "$(Agent.TempDirectory)/ansible_collections"/. "${PWD}/../../../ansible_collections" + displayName: Set up dependencies + - bash: .azure-pipelines/scripts/run-tests.sh "$(entryPoint)" "${{ job.test }}" "$(coverageBranches)" + displayName: Run Tests + - bash: .azure-pipelines/scripts/process-results.sh + condition: succeededOrFailed() + displayName: Process Results + - bash: .azure-pipelines/scripts/aggregate-coverage.sh "$(Agent.TempDirectory)" + condition: eq(variables.haveCoverageData, 'true') + displayName: Aggregate Coverage Data + - task: PublishTestResults@2 + condition: eq(variables.haveTestResults, 'true') + inputs: + testResultsFiles: "$(outputPath)/junit/*.xml" + displayName: Publish Test Results + - task: PublishPipelineArtifact@1 + condition: eq(variables.haveBotResults, 'true') + displayName: Publish Bot Results + inputs: + targetPath: "$(outputPath)/bot/" + artifactName: "Bot $(System.JobAttempt) $(System.StageDisplayName) $(System.JobDisplayName)" + - task: PublishPipelineArtifact@1 + condition: eq(variables.haveCoverageData, 'true') + displayName: Publish Coverage Data + inputs: + targetPath: "$(Agent.TempDirectory)/coverage/" + artifactName: "Coverage $(System.JobAttempt) $(System.StageDisplayName) $(System.JobDisplayName)" diff --git a/ansible_collections/community/windows/.git-blame-ignore-revs b/ansible_collections/community/windows/.git-blame-ignore-revs new file mode 100644 index 000000000..04075b5a8 --- /dev/null +++ b/ansible_collections/community/windows/.git-blame-ignore-revs @@ -0,0 +1,5 @@ +# .git-blame-ignore-revs +# Bulk PowerShell sanity fixes PSSA 1.20.0 +08257cb1863b0c61d80c4a407fef9a9e5d6a8e0b +# Bulk PowerShell sanity fixes PSSA 1.21.0 +47d3207f1004b8b0023ab29ef23e0677bfa9405e diff --git a/ansible_collections/community/windows/.github/BOTMETA.yml b/ansible_collections/community/windows/.github/BOTMETA.yml new file mode 100644 index 000000000..602ccd82e --- /dev/null +++ b/ansible_collections/community/windows/.github/BOTMETA.yml @@ -0,0 +1,339 @@ +automerge: false +files: + $lookups/laps_password.py: + $modules/psexec.py: {} + $modules/win_audit_policy_system.ps1: + maintainers: $team_windows + $modules/win_audit_policy_system.py: + authors: nwsparks + maintainers: $team_windows + $modules/win_audit_rule.ps1: + maintainers: $team_windows + $modules/win_audit_rule.py: + authors: nwsparks + maintainers: $team_windows + $modules/win_auto_logon.ps1: + maintainers: $team_windows + $modules/win_auto_logon.py: + authors: prasoonkarunan + maintainers: $team_windows + $modules/win_certificate_info.ps1: + maintainers: $team_windows + $modules/win_certificate_info.py: + authors: mhunsber + maintainers: $team_windows + $modules/win_computer_description.ps1: + maintainers: $team_windows + $modules/win_computer_description.py: + authors: RusoSova + maintainers: $team_windows + $modules/win_credential.ps1: + maintainers: $team_windows + $modules/win_credential.py: {} + $modules/win_data_deduplication.ps1: + maintainers: $team_windows + $modules/win_data_deduplication.py: + authors: rnsc + maintainers: $team_windows + $modules/win_defrag.ps1: + maintainers: $team_windows + $modules/win_defrag.py: + authors: dagwieers + maintainers: $team_windows + $modules/win_disk_facts.ps1: + maintainers: $team_windows + $modules/win_disk_facts.py: + authors: marqelme + maintainers: $team_windows + $modules/win_disk_image.ps1: + maintainers: $team_windows + $modules/win_disk_image.py: + authors: nitzmahone + maintainers: $team_windows + $modules/win_dns_record.ps1: + maintainers: $team_windows + $modules/win_dns_record.py: + authors: johnboy2 + maintainers: $team_windows + $modules/win_domain_computer.ps1: + maintainers: $team_windows + $modules/win_domain_computer.py: + authors: Daniel-Sanchez-Fabregas + maintainers: $team_windows + $modules/win_domain_group.ps1: + maintainers: $team_windows + $modules/win_domain_group.py: {} + $modules/win_domain_group_membership.ps1: + maintainers: $team_windows + $modules/win_domain_group_membership.py: + authors: jiuka + maintainers: $team_windows + $modules/win_domain_object_info.ps1: + maintainers: $team_windows + $modules/win_domain_object_info.py: {} + $modules/win_domain_user.ps1: + maintainers: $team_windows + $modules/win_domain_user.py: + authors: nwchandler + maintainers: $team_windows + $modules/win_dotnet_ngen.ps1: + maintainers: $team_windows + $modules/win_dotnet_ngen.py: + authors: petemounce + maintainers: $team_windows + $modules/win_eventlog.ps1: + maintainers: $team_windows + $modules/win_eventlog.py: + authors: andrewsaraceni + maintainers: $team_windows + $modules/win_eventlog_entry.ps1: + maintainers: $team_windows + $modules/win_eventlog_entry.py: + authors: andrewsaraceni + maintainers: $team_windows + $modules/win_file_compression.ps1: + maintainers: $team_windows + $modules/win_file_compression.py: + authors: mhunsber + maintainers: $team_windows + $modules/win_file_version.ps1: + maintainers: $team_windows + $modules/win_file_version.py: + authors: SamLiu79 + maintainers: $team_windows + $modules/win_firewall.ps1: + maintainers: $team_windows + $modules/win_firewall.py: + authors: michaeldeaton + maintainers: $team_windows + $modules/win_firewall_rule.ps1: + maintainers: $team_windows + $modules/win_firewall_rule.py: + authors: TimothyVandenbrande ar7z1 + maintainers: $team_windows + $modules/win_format.ps1: + maintainers: $team_windows + $modules/win_format.py: + authors: chopraaa + maintainers: $team_windows + $modules/win_hosts.ps1: + maintainers: $team_windows + $modules/win_hosts.py: + authors: mhunsber + maintainers: $team_windows + $modules/win_hotfix.ps1: + maintainers: $team_windows + $modules/win_hotfix.py: {} + $modules/win_http_proxy.ps1: + maintainers: $team_windows + $modules/win_http_proxy.py: {} + $modules/win_iis_virtualdirectory.ps1: + maintainers: $team_windows + $modules/win_iis_virtualdirectory.py: + authors: henrikwallstrom + maintainers: $team_windows + $modules/win_iis_webapplication.ps1: + maintainers: $team_windows + $modules/win_iis_webapplication.py: + authors: henrikwallstrom + maintainers: $team_windows + $modules/win_iis_webapppool.ps1: + maintainers: $team_windows + $modules/win_iis_webapppool.py: + authors: henrikwallstrom jborean93 + $modules/win_iis_webbinding.ps1: + maintainers: $team_windows + $modules/win_iis_webbinding.py: + authors: henrikwallstrom nwsparks + maintainers: $team_windows + $modules/win_iis_website.ps1: + maintainers: $team_windows + $modules/win_iis_website.py: + authors: henrikwallstrom + maintainers: $team_windows + $modules/win_inet_proxy.ps1: + maintainers: $team_windows + $modules/win_inet_proxy.py: {} + $modules/win_initialize_disk.ps1: + maintainers: $team_windows + $modules/win_initialize_disk.py: + authors: branic + maintainers: $team_windows + $modules/win_lineinfile.ps1: + maintainers: $team_windows + $modules/win_lineinfile.py: + authors: brianlloyd + maintainers: $team_windows + $modules/win_mapped_drive.ps1: + maintainers: $team_windows + $modules/win_mapped_drive.py: {} + $modules/win_msg.ps1: + maintainers: $team_windows + $modules/win_msg.py: + authors: jhawkesworth + maintainers: ShachafGoldstein jborean93 + $modules/win_netbios.ps1: + maintainers: $team_windows + $modules/win_netbios.py: + authors: tmmruk + maintainers: $team_windows + $modules/win_nssm.ps1: + maintainers: $team_windows + $modules/win_nssm.py: + authors: Shachaf92 georgefrank h0nIg ksubileau smadam813 themiwi + maintainers: $team_windows + ignore: georgefrank h0nIg + $modules/win_pagefile.ps1: + maintainers: $team_windows + $modules/win_pagefile.py: + authors: LiranNis + maintainers: $team_windows + $modules/win_partition.ps1: + maintainers: $team_windows + $modules/win_partition.py: + authors: chopraaa + maintainers: $team_windows + $modules/win_pester.ps1: + maintainers: $team_windows + $modules/win_pester.py: + authors: equelin prasoonkarunan + maintainers: $team_windows + $modules/win_power_plan.ps1: + maintainers: $team_windows + $modules/win_power_plan.py: + authors: nwsparks + maintainers: $team_windows + $modules/win_product_facts.ps1: + maintainers: $team_windows + $modules/win_product_facts.py: + authors: dagwieers + maintainers: $team_windows + $modules/win_psexec.ps1: + maintainers: $team_windows + $modules/win_psexec.py: + authors: dagwieers + maintainers: $team_windows + $modules/win_psmodule.ps1: + maintainers: $team_windows + $modules/win_psmodule.py: + authors: dlazz it-praktyk + maintainers: $team_windows + $modules/win_psrepository.ps1: + maintainers: $team_windows + $modules/win_psrepository.py: + authors: it-praktyk + maintainers: $team_windows + $modules/win_psrepository_info.ps1: + maintainers: $team_windows + $modules/win_psrepository_info.py: + authors: briantist + maintainers: $team_windows + $modules/win_rabbitmq_plugin.ps1: + maintainers: $team_windows + $modules/win_rabbitmq_plugin.py: + authors: ar7z1 + maintainers: $team_windows + $modules/win_rds_cap.ps1: + maintainers: $team_windows + $modules/win_rds_cap.py: + authors: ksubileau + maintainers: $team_windows + $modules/win_rds_rap.ps1: + maintainers: $team_windows + $modules/win_rds_rap.py: + authors: ksubileau + maintainers: $team_windows + $modules/win_rds_settings.ps1: + maintainers: $team_windows + $modules/win_rds_settings.py: + authors: ksubileau + maintainers: $team_windows + $modules/win_region.ps1: + maintainers: $team_windows + $modules/win_region.py: {} + $modules/win_regmerge.ps1: + maintainers: $team_windows + $modules/win_regmerge.py: + authors: jhawkesworth + maintainers: ShachafGoldstein jborean93 + $modules/win_robocopy.ps1: + maintainers: $team_windows + $modules/win_robocopy.py: + authors: blakfeld + maintainers: $team_windows + $modules/win_route.ps1: + maintainers: $team_windows + $modules/win_route.py: + authors: dlazz + maintainers: $team_windows + $modules/win_say.ps1: + maintainers: $team_windows + $modules/win_say.py: + authors: jhawkesworth + maintainers: ShachafGoldstein jborean93 + $modules/win_scheduled_task.ps1: + maintainers: $team_windows + $modules/win_scheduled_task.py: + authors: jborean93 petemounce + $modules/win_scheduled_task_stat.ps1: + maintainers: $team_windows + $modules/win_scheduled_task_stat.py: {} + $modules/: + notify: if-meaton + authors: jborean93 + maintainers: ShachafGoldstein jhawkesworth + labels: win_security_policy + ignore: georgefrank ryansb + keywords: credssp hyperv powershell psrp winrm + $modules/win_shortcut.ps1: + maintainers: $team_windows + $modules/win_shortcut.py: + authors: dagwieers + maintainers: $team_windows + $modules/win_snmp.ps1: + maintainers: $team_windows + $modules/win_snmp.py: + authors: mcassaniti + maintainers: $team_windows + $modules/win_timezone.ps1: + maintainers: $team_windows + $modules/win_timezone.py: + authors: schwartzmx + maintainers: $team_windows + $modules/win_toast.ps1: + maintainers: $team_windows + $modules/win_toast.py: + authors: jhawkesworth + maintainers: ShachafGoldstein jborean93 + $modules/win_unzip.ps1: + maintainers: $team_windows + $modules/win_unzip.py: + authors: schwartzmx + maintainers: $team_windows + $modules/win_user_profile.ps1: + maintainers: $team_windows + $modules/win_user_profile.py: {} + $modules/win_wait_for_process.ps1: + maintainers: $team_windows + $modules/win_wait_for_process.py: + authors: crossan007 + maintainers: $team_windows + $modules/win_wakeonlan.ps1: + maintainers: $team_windows + $modules/win_wakeonlan.py: + authors: dagwieers + maintainers: $team_windows + $modules/win_webpicmd.ps1: + maintainers: $team_windows + $modules/win_webpicmd.py: + authors: petemounce + maintainers: $team_windows + $modules/win_xml.ps1: + maintainers: $team_windows + $modules/win_xml.py: + authors: jhawkesworth richardcs + maintainers: ShachafGoldstein jborean93 +macros: + lookups: plugins/lookup + modules: plugins/modules + team_windows: ShachafGoldstein jborean93 jhawkesworth diff --git a/ansible_collections/community/windows/.github/workflows/docs-pr.yml b/ansible_collections/community/windows/.github/workflows/docs-pr.yml new file mode 100644 index 000000000..3b89bc6a7 --- /dev/null +++ b/ansible_collections/community/windows/.github/workflows/docs-pr.yml @@ -0,0 +1,67 @@ +name: Collection Docs +concurrency: + group: docs-pr-${{ github.head_ref }} + cancel-in-progress: true +on: + pull_request_target: + types: [opened, synchronize, reopened, closed] + +env: + GHP_BASE_URL: https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }} + +jobs: + # Validation job runs a strict build to ensure it will fail CI on any mistakes. + validate-docs: + permissions: + contents: read + name: Validate Ansible Docs + if: github.event.action != 'closed' + uses: ansible-community/github-docs-build/.github/workflows/_shared-docs-build-push.yml@main + with: + artifact-upload: false + init-lenient: false + init-fail-on-error: true + build-ref: refs/pull/${{ github.event.number }}/merge + + # The build job runs with the most lenient settings to ensure the best chance of getting + # a rendered docsite that can be looked at. + build-docs: + permissions: + contents: read + name: Build Ansible Docs + uses: ansible-community/github-docs-build/.github/workflows/_shared-docs-build-pr.yml@main + with: + init-lenient: true + init-fail-on-error: false + + comment: + permissions: + pull-requests: write + runs-on: ubuntu-latest + needs: [build-docs] + name: PR comments + steps: + - name: PR comment + uses: ansible-community/github-docs-build/actions/ansible-docs-build-comment@main + with: + body-includes: '## Docs Build' + reactions: heart + action: ${{ needs.build-docs.outputs.changed != 'true' && 'remove' || '' }} + on-closed-action: remove + on-merged-action: remove + body: | + ## Docs Build 📝 + + Thank you for contribution!✨ + + The docsite for **this PR** is available for download as an artifact from this run: + ${{ needs.build-docs.outputs.artifact-url }} + + You can compare to the docs for the `main` branch here: + ${{ env.GHP_BASE_URL }}/branch/main + + File changes: + + ${{ needs.build-docs.outputs.diff-files-rendered }} + + ${{ needs.build-docs.outputs.diff-rendered }} diff --git a/ansible_collections/community/windows/.github/workflows/docs-push.yml b/ansible_collections/community/windows/.github/workflows/docs-push.yml new file mode 100644 index 000000000..a59f1553d --- /dev/null +++ b/ansible_collections/community/windows/.github/workflows/docs-push.yml @@ -0,0 +1,35 @@ +name: Collection Docs +concurrency: + group: docs-push-${{ github.sha }} + cancel-in-progress: true +on: + push: + branches: + - main + tags: + - '*' + schedule: + - cron: '0 13 * * *' + +jobs: + build-docs: + permissions: + contents: read + name: Build Ansible Docs + uses: ansible-community/github-docs-build/.github/workflows/_shared-docs-build-push.yml@main + with: + init-lenient: false + init-fail-on-error: true + + publish-docs-gh-pages: + # use to prevent running on forks + if: github.repository == 'ansible-collections/community.windows' + permissions: + contents: write + needs: [build-docs] + name: Publish Ansible Docs + uses: ansible-community/github-docs-build/.github/workflows/_shared-docs-build-publish-gh-pages.yml@main + with: + artifact-name: ${{ needs.build-docs.outputs.artifact-name }} + secrets: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/ansible_collections/community/windows/.github/workflows/stale.yml b/ansible_collections/community/windows/.github/workflows/stale.yml new file mode 100644 index 000000000..aef40d7fd --- /dev/null +++ b/ansible_collections/community/windows/.github/workflows/stale.yml @@ -0,0 +1,23 @@ +name: Stale pull request handler +on: + schedule: + - cron: 0 0 * * * + +permissions: + pull-requests: write + +jobs: + stale: + runs-on: ubuntu-latest + + steps: + - uses: actions/stale@v4.0.0 + id: stale + with: + days-before-stale: -1 + days-before-pr-stale: 28 + days-before-pr-close: 14 + stale-pr-label: stale + stale-pr-message: >- + This pull request is stale because it has been open for 4 weeks with no activity. + Remove stale label or comment or this will be closed in 2 weeks. diff --git a/ansible_collections/community/windows/.gitignore b/ansible_collections/community/windows/.gitignore new file mode 100644 index 000000000..86c95b8d2 --- /dev/null +++ b/ansible_collections/community/windows/.gitignore @@ -0,0 +1,514 @@ + +# Created by https://www.gitignore.io/api/git,linux,pydev,python,windows,pycharm+all,jupyternotebook,vim,webstorm,emacs,dotenv,macos +# Edit at https://www.gitignore.io/?templates=git,linux,pydev,python,windows,pycharm+all,jupyternotebook,vim,webstorm,emacs,dotenv,macos + +### dotenv ### +.env + +### Emacs ### +# -*- mode: gitignore; -*- +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* + +# Org-mode +.org-id-locations +*_archive + +# flymake-mode +*_flymake.* + +# eshell files +/eshell/history +/eshell/lastdir + +# elpa packages +/elpa/ + +# reftex files +*.rel + +# AUCTeX auto folder +/auto/ + +# cask packages +.cask/ +dist/ + +# Flycheck +flycheck_*.el + +# server auth directory +/server/ + +# projectiles files +.projectile + +# directory configuration +.dir-locals.el + +# network security +/network-security.data + + +### Git ### +# Created by git for backups. To disable backups in Git: +# $ git config --global mergetool.keepBackup false +*.orig + +# Created by git when using merge tools for conflicts +*.BACKUP.* +*.BASE.* +*.LOCAL.* +*.REMOTE.* +*_BACKUP_*.txt +*_BASE_*.txt +*_LOCAL_*.txt +*_REMOTE_*.txt + +#!! ERROR: jupyternotebook is undefined. Use list command to see defined gitignore types !!# + +### Linux ### + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### PyCharm+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### PyCharm+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### pydev ### +.pydevproject + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + + +### Vim ### +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +### WebStorm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff + +# AWS User-specific + +# Generated files + +# Sensitive or high-churn files + +# Gradle + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake + +# Mongo Explorer plugin + +# File-based project format + +# IntelliJ + +# mpeltonen/sbt-idea plugin + +# JIRA plugin + +# Cursive Clojure plugin + +# SonarLint plugin + +# Crashlytics plugin (for Android Studio and IntelliJ) + +# Editor-based Rest Client + +# Android studio 3.1+ serialized cache file + +### WebStorm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.gitignore.io/api/git,linux,pydev,python,windows,pycharm+all,jupyternotebook,vim,webstorm,emacs,dotenv,macos + +# Changelog cache files +changelogs/.plugin-cache.yaml + +# ansible-test ignores +tests/integration/inventory* diff --git a/ansible_collections/community/windows/.vscode/extensions.json b/ansible_collections/community/windows/.vscode/extensions.json new file mode 100644 index 000000000..a877e78b7 --- /dev/null +++ b/ansible_collections/community/windows/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "ms-python.python", + "ms-vscode.powershell", + ] +} diff --git a/ansible_collections/community/windows/.vscode/settings.json b/ansible_collections/community/windows/.vscode/settings.json new file mode 100644 index 000000000..6fde5838c --- /dev/null +++ b/ansible_collections/community/windows/.vscode/settings.json @@ -0,0 +1,30 @@ +{ + "python.testing.pytestArgs": [ + "tests/unit", + "-vv" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "python.analysis.extraPaths": [ + "${workspaceFolder}/../../../", + ], + "powershell.codeFormatting.addWhitespaceAroundPipe": true, + "powershell.codeFormatting.alignPropertyValuePairs": false, + "powershell.codeFormatting.autoCorrectAliases": true, + "powershell.codeFormatting.ignoreOneLineBlock": true, + "powershell.codeFormatting.newLineAfterCloseBrace": true, + "powershell.codeFormatting.newLineAfterOpenBrace": true, + "powershell.codeFormatting.openBraceOnSameLine": true, + "powershell.codeFormatting.pipelineIndentationStyle": "IncreaseIndentationForFirstPipeline", + "powershell.codeFormatting.whitespaceAfterSeparator": true, + "powershell.codeFormatting.whitespaceAroundOperator": true, + "powershell.codeFormatting.whitespaceBeforeOpenBrace": true, + "powershell.codeFormatting.whitespaceBeforeOpenParen": true, + "powershell.codeFormatting.whitespaceBetweenParameters": true, + "powershell.codeFormatting.whitespaceInsideBrace": true, + "powershell.codeFormatting.avoidSemicolonsAsLineTerminators": true, + "powershell.scriptAnalysis.enable": true, + "[powershell]": { + "editor.formatOnSave": true, + }, +}
\ No newline at end of file diff --git a/ansible_collections/community/windows/CHANGELOG.rst b/ansible_collections/community/windows/CHANGELOG.rst new file mode 100644 index 000000000..45b4f6eaa --- /dev/null +++ b/ansible_collections/community/windows/CHANGELOG.rst @@ -0,0 +1,367 @@ +=============================== +Community Windows Release Notes +=============================== + +.. contents:: Topics + + +v1.13.0 +======= + +Release Summary +--------------- + +Release summary for v1.13.0 + +Minor Changes +------------- + +- Raise minimum Ansible version to ``2.12`` or newer +- win_dns_record - Add parameter ``aging`` for creating non-static DNS records. +- win_domain_computer - Add ActiveDirectory module import +- win_domain_object_info - Add ActiveDirectory module import +- win_psmodule - add ``force`` option to allow overwriting/updating existing module dependency only if requested +- win_pssession_configuration - Add diff mode support + +Bugfixes +-------- + +- win_disk_facts - Fix issue when enumerating non-physical disks or disks without numbers - https://github.com/ansible-collections/community.windows/issues/474 +- win_firewall_rule - fix program cannot be set to any on existing rules. +- win_psmodule - Fix missing AcceptLicense parameter that occurs when the pre-reqs have been installed - https://github.com/ansible-collections/community.windows/issues/487 +- win_pssession_configuration - Fix parser error (Invalid JSON primitive: icrosoft.WSMan.Management.WSManConfigContainerElement) +- win_xml - Fixes the issue when no childnode is defined and will allow adding a new element to an empty element. +- win_zip - fix source appears to use backslashes as path separators issue when extracting Zip archve in non-Windows environment - https://github.com/ansible-collections/community.windows/issues/442 + +v1.12.0 +======= + +Release Summary +--------------- + +Release summary for v1.12.0 + +Minor Changes +------------- + +- win_dns_record - Added support for DHCID (RFC 4701) records +- win_domain_user - Added the ``display_name`` option to set the users display name attribute + +Bugfixes +-------- + +- win_firewall_rule - fix problem in check mode with multiple ip addresses not in same order +- win_partition - fix problem in auto assigning a drive letter should the user use either a, u, t or o as a drive letter + +v1.11.1 +======= + +Release Summary +--------------- + +Release summary for v1.11.1 + +Bugfixes +-------- + +- win_dhcp_lease - call Get-DhcpServerv4Lease once when MAC and IP are defined (https://github.com/ansible-collections/community.windows/pull/427) +- win_dhcp_lease - fix mac address convert (https://github.com/ansible-collections/community.windows/issues/291) +- win_psmodule - Fix bootstrapping PowerShellGet with ``-AcceptLicense`` - https://github.com/ansible-collections/community.windows/issues/424 +- win_psmodule - Source PowerShellGet and PackagementManagement from ``repository`` if specified +- win_region - did not allow regional format en-150 (= English(Europe); also referred as en-EU or en-Europe). This fix allows specifying en-150 as regional format (https://github.com/ansible-collections/community.windows/issues/438). +- win_scoop - Fix idempotency checks with Scoop ``v0.2.3`` and newer. + +v1.11.0 +======= + +Release Summary +--------------- + +Release summary for v1.11.0 + +Minor Changes +------------- + +- Raise minimum Ansible version to ``2.11`` or newer +- win_psmodule module - add ``accept_license`` option to allow for installing modules that require license acceptance (https://github.com/ansible-collections/community.windows/issues/340). + +Bugfixes +-------- + +- win_domain_user - Fix broken warning call when failing to get group membership - https://github.com/ansible-collections/community.windows/issues/412 +- win_scheduled_task - Fix the Monthly DOW trigger value ``run_on_last_week_of_month`` when ``weeks_of_month`` is also set - https://github.com/ansible-collections/community.windows/issues/414 + +v1.10.0 +======= + +Release Summary +--------------- + +Release summary for v1.10.0 + +Minor Changes +------------- + +- win_domain_user - Add support for managing service prinicpal names via the ``spn`` param and principals allowed to delegate via the ``delegates`` param (https://github.com/ansible-collections/community.windows/pull/365) +- win_domain_user - Added the ``groups_missing_behaviour`` option that controls the behaviour when a group specified does not exist - https://github.com/ansible-collections/community.windows/pull/375 +- win_hotfix - Added the ``identifiers`` and ``kbs`` return value that is always a list of identifiers and kbs inside a hotfix +- win_psmodule - Add credential support for through the ``username`` and ``password`` options +- win_psrepository - Add credential support for through the ``username`` and ``password`` options + +Bugfixes +-------- + +- win_hotfix - Supports hotfixes that contain multiple updates inside the supplied update msu - https://github.com/ansible-collections/community.windows/issues/284 +- win_iis_webapplication - Fix physical path check for broken configurations - https://github.com/ansible-collections/community.windows/pull/385 +- win_rds_cap - Fix SID lookup with any account ending with the ``@builtin`` UPN suffix +- win_rds_rap - Fix SID lookup with any account ending with the ``@builtin`` UPN suffix +- win_region - Fix junk output when copying settings across users +- win_scoop - Fix bootstrapping process to properly work when running as admin +- win_scoop_bucket - Fix handling of output and errors from each scoop command + +New Modules +----------- + +- win_listen_ports_facts - Recopilates the facts of the listening ports of the machine + +v1.9.0 +====== + +Minor Changes +------------- + +- win_disk_facts - Added ``filter`` option to filter returned facts by type of disk information - https://github.com/ansible-collections/community.windows/issues/33 +- win_disk_facts - Converted from ``#Requires -Module Ansible.ModuleUtils.Legacy`` to ``#AnsibleRequires -CSharpUtil Ansible.Basic`` +- win_iis_virtualdirectory - Added the ``connect_as``, ``username``, and ``password`` options to control the virtual directory authentication - https://github.com/ansible-collections/community.windows/issues/346 +- win_power_plan - Added ``guid`` option to specify plan by a unique identifier - https://github.com/ansible-collections/community.windows/issues/310 + +Bugfixes +-------- + +- win_domain_user - Module now properly captures and reports bad password - https://github.com/ansible-collections/community.windows/issues/316 +- win_domain_user - Module now reports user created and changed properly - https://github.com/ansible-collections/community.windows/issues/316 +- win_domain_user - The AD user's existing identity is searched using their sAMAccountName name preferentially and falls back to the provided name property instead - https://github.com/ansible-collections/community.windows/issues/344 +- win_iis_virtualdirectory - Fixed an issue where virtual directory information could not be obtained correctly when the parameter ``application`` was set + +v1.8.0 +====== + +Minor Changes +------------- + +- win_nssm - Added ``username`` as an alias for ``user`` +- win_nssm - Remove deprecation for ``state``, ``dependencies``, ``user``, ``password``, ``start_mode`` +- win_nssm - Support gMSA accounts for ``user`` + +Bugfixes +-------- + +- win_audit_rule - Fix exception when trying to change a rule on a hidden or protected system file - https://github.com/ansible-collections/community.windows/issues/17 +- win_firewall - Fix GpoBoolean/Boolean comparation(windows versions compatibility increase) +- win_nssm - Perform better user comparison checks for idempotency +- win_pssession_configuration - the associated action plugin detects check mode using a method that isn't always accurate (https://github.com/ansible-collections/community.windows/pull/318). +- win_region - Fix conflicts with existing ``LIB`` environment variable +- win_scheduled_task - Fix conflicts with existing ``LIB`` environment variable +- win_scheduled_task_stat - Fix conflicts with existing ``LIB`` environment variable +- win_scoop_bucket - Ensure no extra data is sent to the controller resulting in a junk output warning +- win_xml - Do not show warnings for normal operations - https://github.com/ansible-collections/community.windows/issues/205 +- win_xml - Fix removal operation when running with higher verbosities - https://github.com/ansible-collections/community.windows/issues/275 + +New Modules +----------- + +- win_domain_ou - Manage Active Directory Organizational Units + +v1.7.0 +====== + +Minor Changes +------------- + +- win_domain_user - Added ``sam_account_name`` to explicitly set the ``sAMAccountName`` property of an object - https://github.com/ansible-collections/community.windows/issues/281 + +Bugfixes +-------- + +- win_dns_record - Fix issue when trying to use the ``computer_name`` option - https://github.com/ansible-collections/community.windows/issues/276 +- win_domain_user - Fallback to NETBIOS username for password verification check if the UPN is not set - https://github.com/ansible-collections/community.windows/pull/289 +- win_initialize_disk - Ensure ``online: False`` doesn't bring the disk online again - https://github.com/ansible-collections/community.windows/pull/268 +- win_lineinfile - Fix up diff output with ending newlines - https://github.com/ansible-collections/community.windows/pull/283 +- win_region - Fix ``copy_settings`` on a host that has disabled ``reg.exe`` access - https://github.com/ansible-collections/community.windows/issues/287 + +v1.6.0 +====== + +Minor Changes +------------- + +- win_dns_record - Added txt Support +- win_scheduled_task - Added support for setting a ``session_state_change`` trigger by documenting the human friendly values for ``state_change`` +- win_scheduled_task_state - Added ``state_change_str`` to the trigger output to give a human readable description of the value + +Security Fixes +-------------- + +- win_psexec - Ensure password is masked in ``psexec_command`` return result - https://github.com/ansible-collections/community.windows/issues/43 + +v1.5.0 +====== + +Bugfixes +-------- + +- win_dns_zone - Fix idempotency when using a DNS zone with forwarders - https://github.com/ansible-collections/community.windows/issues/259 +- win_domain_group_member - Fix faulty logic when comparing existing group members - https://github.com/ansible-collections/community.windows/issues/256 +- win_lineinfile - Avoid stripping the newline at the end of a file - https://github.com/ansible-collections/community.windows/pull/219 +- win_product_facts - fixed an issue that the module doesn't correctly convert a product id (https://github.com/ansible-collections/community.windows/pull/251). + +v1.4.0 +====== + +Bugfixes +-------- + +- win_domain_group_membership - Handle timeouts when dealing with group with lots of members - https://github.com/ansible-collections/community.windows/pull/204 +- win_domain_user - Make sure a password is set to change when it is marked as password needs to be changed before logging in - https://github.com/ansible-collections/community.windows/issues/223 +- win_domain_user - fix reporting on user when running in check mode - https://github.com/ansible-collections/community.windows/pull/248 +- win_lineinfile - Fix crash when using ``insertbefore`` and ``insertafter`` at the same time - https://github.com/ansible-collections/community.windows/issues/220 +- win_partition - Fix gtp_type setting in win_partition - https://github.com/ansible-collections/community.windows/issues/241 +- win_psmodule - Makes sure ``-AllowClobber`` is used when updating pre-requisites if requested - https://github.com/ansible-collections/community.windows/issues/42 +- win_pssession_configuration - the ``async_poll`` option was not actually used and polling mode was always used with the default poll delay; this change also formally disables ``async_poll=0`` (https://github.com/ansible-collections/community.windows/pull/212). +- win_wait_for_process - Fix bug when specifying multiple ``process_name_exact`` values - https://github.com/ansible-collections/community.windows/issues/203 + +New Modules +----------- + +- win_feature_info - Gather information about Windows features + +v1.3.0 +====== + +Minor Changes +------------- + +- Extend win_domain_computer adding managedBy parameter. + +Bugfixes +-------- + +- win_firewall_rule - Ensure ``service: any`` is set to match any service instead of the literal service called ``any`` as per the docs +- win_scoop - Make sure we enable TLS 1.2 when installing scoop +- win_xml - Fix ``PropertyNotFound`` exception when creating a new attribute - https://github.com/ansible-collections/community.windows/issues/166 + +New Modules +----------- + +- win_psrepository_copy - Copies registered PSRepositories to other user profiles + +v1.2.0 +====== + +Minor Changes +------------- + +- win_nssm - added new parameter 'app_environment' for managing service environment. +- win_scheduled_task - validate task name against invalid characters (https://github.com/ansible-collections/community.windows/pull/168) +- win_scheduled_task_stat - add check mode support (https://github.com/ansible-collections/community.windows/pull/167) + +Bugfixes +-------- + +- win_partition - fix size comparison errors when size specified in bytes (https://github.com/ansible-collections/community.windows/pull/159) +- win_security_policy - read config file with correct encoding to avoid breaking non-ASCII chars +- win_security_policy - strip of null char added by secedit for ``LegalNoticeText`` so the existing value is preserved + +New Modules +----------- + +- win_net_adapter_feature - Enable or disable certain network adapters. + +v1.1.0 +====== + +Minor Changes +------------- + +- win_dns_record - Support NS record creation,modification and deletion +- win_firewall - Support defining the default inbound and outbound action of traffic in Windows firewall. +- win_psrepository - Added the ``proxy`` option that defines the proxy to use for the repository being managed + +v1.0.0 +====== + +Minor Changes +------------- + +- win_dns_record - Added support for managing ``SRV`` records +- win_firewall_rule - Support editing rules by the group it belongs to +- win_firewall_rule - Support editing rules that have a duplicated name + +Breaking Changes / Porting Guide +-------------------------------- + +- win_pester - no longer runs all ``*.ps1`` file in the directory specified due to it executing potentially unknown scripts. It will follow the default behaviour of only running tests for files that are like ``*.tests.ps1`` which is built into Pester itself. + +Removed Features (previously deprecated) +---------------------------------------- + +- win_psexec - removed the deprecated ``extra_opts`` option. + +Bugfixes +-------- + +- win_scoop - add checks for globally installed packages for better idempotency checks + +New Modules +----------- + +- win_scoop_bucket - Manage Scoop buckets + +v0.2.0 +====== + +Release Summary +--------------- + +This is the first proper release of the ``community.windows`` collection on 2020-07-18. +The changelog describes all changes made to the modules and plugins included in this collection since Ansible 2.9.0. + + +Minor Changes +------------- + +- win_disk_facts - Set output array order to be by disk number property - https://github.com/ansible/ansible/issues/63998 +- win_domain_computer - ``sam_account_name`` with missing ``$`` will have it added automatically (https://github.com/ansible-collections/community.windows/pull/93) +- win_domain_computer - add support for offline domain join (https://github.com/ansible-collections/community.windows/pull/93) +- win_domain_group_membership - Add multi-domain forest support - https://github.com/ansible/ansible/issues/59829 +- win_domain_user - Added the ``identity`` module option to explicitly set the identity of the user when searching for it - https://github.com/ansible/ansible/issues/45298 +- win_firewall- Change req check from wmf version to cmdlets presence - https://github.com/ansible/ansible/issues/63003 +- win_firewall_rule - add parameter to support ICMP Types and Codes (https://github.com/ansible/ansible/issues/46809) +- win_iis_webapplication - add new options ``connect_as``, ``username``, ``password``. +- win_iis_webapplication - now uses the current application pool of the website instead of the DefaultAppPool if none was specified. +- win_nssm - Implement additional parameters - (https://github.com/ansible/ansible/issues/62620) +- win_pester - Only execute ``*.tests.ps1`` in ``path`` to match the default behaviour in Pester - https://github.com/ansible/ansible/issues/55736 + +Removed Features (previously deprecated) +---------------------------------------- + +- win_disk_image - removed the deprecated return value ``mount_path`` in favour of ``mount_paths``. + +Bugfixes +-------- + +- **security issue** win_unzip - normalize paths in archive to ensure extracted files do not escape from the target directory (CVE-2020-1737) +- psexec - Fix issue where the Kerberos package was not detected as being available. +- psexec - Fix issue where the ``interactive`` option was not being passed down to the library. +- win_credential - Fix issue that errors when trying to add a ``name`` with wildcards. +- win_domain_computer - Fix idempotence checks when ``sAMAccountName`` is different from ``name`` +- win_domain_computer - Honour the explicit domain server and credentials when moving or removing a computer object - https://github.com/ansible/ansible/pull/63093 +- win_domain_user - Better handle cases when getting a new user's groups fail - https://github.com/ansible/ansible/issues/54331 +- win_format - Idem not working if file exist but same fs (https://github.com/ansible/ansible/issues/58302) +- win_format - fixed issue where module would not change allocation unit size (https://github.com/ansible/ansible/issues/56961) +- win_iis_webapppool - Do not try and set attributes in check mode when the pool did not exist +- win_iis_website - Actually restart the site when ``state=restarted`` - https://github.com/ansible/ansible/issues/63828 +- win_partition - Fix invalid variable name causing a failure on checks - https://github.com/ansible/ansible/issues/62401 +- win_partition - don't resize partitions if size difference is < 1 MiB +- win_timezone - Allow for _dstoff timezones +- win_unzip - Fix support for paths with square brackets not being detected properly diff --git a/ansible_collections/community/windows/COPYING b/ansible_collections/community/windows/COPYING new file mode 100644 index 000000000..10926e87f --- /dev/null +++ b/ansible_collections/community/windows/COPYING @@ -0,0 +1,675 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program 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. + + This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<http://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<http://www.gnu.org/philosophy/why-not-lgpl.html>. + diff --git a/ansible_collections/community/windows/FILES.json b/ansible_collections/community/windows/FILES.json new file mode 100644 index 000000000..284a5a7ca --- /dev/null +++ b/ansible_collections/community/windows/FILES.json @@ -0,0 +1,6760 @@ +{ + "files": [ + { + "name": ".", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": ".azure-pipelines", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": ".azure-pipelines/scripts", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": ".azure-pipelines/scripts/aggregate-coverage.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "820353ffde6fd3ad655118772547549d84ccf0a7ba951e8fb1325f912ef640a0", + "format": 1 + }, + { + "name": ".azure-pipelines/scripts/combine-coverage.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e34d4e863a65b9f53c4ca8ae37655858969898a949e050e9cb3cb0d5f02342d0", + "format": 1 + }, + { + "name": ".azure-pipelines/scripts/process-results.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c03d7273fe58882a439b6723e92ab89f1e127772b5ce35aa67c546dd62659741", + "format": 1 + }, + { + "name": ".azure-pipelines/scripts/publish-codecov.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "70c795c8dbca2534b7909b17911630b7afaa693bbd7154e63a51340bc8b28dad", + "format": 1 + }, + { + "name": ".azure-pipelines/scripts/report-coverage.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ac95befdb1df1de3c24fc33d13860ab728c85717e7b0a3ff44efd2de69b39760", + "format": 1 + }, + { + "name": ".azure-pipelines/scripts/run-tests.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cb08a3ec5715b00d476ae6d63ca22e11a9ad8887239439937d2a7ea342e5a623", + "format": 1 + }, + { + "name": ".azure-pipelines/scripts/time-command.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0232f415efeb583ddff907c058986963b775441eaf129d7162aee0acb0d36834", + "format": 1 + }, + { + "name": ".azure-pipelines/templates", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": ".azure-pipelines/templates/coverage.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "825e1b353858bee17c0d2635dde8587db161cc8b64ba5f0ee3cf5f93d3665316", + "format": 1 + }, + { + "name": ".azure-pipelines/templates/matrix.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4fb0d3ffb2125d5806c7597e4f9d4b2af69cf8c337e9d57803081eddd4a6b081", + "format": 1 + }, + { + "name": ".azure-pipelines/templates/test.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c0e4fe43b02900122ba055fff4a117b553a37ed5797b66162487fed41d9881c3", + "format": 1 + }, + { + "name": ".azure-pipelines/azure-pipelines.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ada52203f7901d9d80e8d41e6eebc59286cf5714861b6116f0f97e3b5a1b3ef6", + "format": 1 + }, + { + "name": ".github", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": ".github/workflows", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": ".github/workflows/docs-pr.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "391d92c0465a8e47effc66a9cf324f29d0c4e2a930f88a51e3b59f293f3bd5af", + "format": 1 + }, + { + "name": ".github/workflows/docs-push.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ba71916285a82cf9cacc58f577512499bb43f41fd2ba27c00ac874218bae07a0", + "format": 1 + }, + { + "name": ".github/workflows/stale.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3cc60f9f80d3ba1aa09b8a9567584d3afbe528096b3d51d54fd6d5c4059f6853", + "format": 1 + }, + { + "name": ".github/BOTMETA.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2c32ef45c7b616a43cad97d178397684e5b51fdef5bff9a4cd72f61854facc26", + "format": 1 + }, + { + "name": ".vscode", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": ".vscode/extensions.json", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "de7876569af783e48703301975f12738dc1d3394444a847848c285370c6acfc7", + "format": 1 + }, + { + "name": ".vscode/settings.json", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3696475b7c6f9a1085de7c88e450266341d2d9f045a30d5e9ca02ee45b505970", + "format": 1 + }, + { + "name": "changelogs", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "changelogs/fragments", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "changelogs/fragments/.keep", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "changelogs/changelog.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5419d8ff3c4a72c1f34b09c65c2eb2ce65de539ca84131eb529ecc3f42c26796", + "format": 1 + }, + { + "name": "changelogs/config.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8f873bb78d459bee3cdc629a2699cbf3ee098b5fb23a9638debb3b6329a3c4cc", + "format": 1 + }, + { + "name": "docs", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "docs/docsite", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "docs/docsite/links.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6ac5521793ff1da0855fc9d3aea10922713fe422acab264653448cd06940d83f", + "format": 1 + }, + { + "name": "meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "meta/runtime.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b3ac5ce6c7ef3a7d47cad35d9a0b4256e079e80aed408946b73baf607016ce00", + "format": 1 + }, + { + "name": "plugins", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/action", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/action/win_pssession_configuration.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2553dc6599f28b0a11233216058a7fc800e8edea8c37999d9e9e91825438dd96", + "format": 1 + }, + { + "name": "plugins/lookup", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/lookup/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "plugins/lookup/laps_password.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "72b045b9842cf67811137401cdf81535960345deac6636fd94b61f642d89be73", + "format": 1 + }, + { + "name": "plugins/modules", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/modules/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "plugins/modules/psexec.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a156009dfc461486b41ddfa6b4693cc641873b89937ae03da7002ab4200e5997", + "format": 1 + }, + { + "name": "plugins/modules/win_audit_policy_system.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6d00aef30a1767778a91b51ae54310210fde0399e66532a7e6ace3ea7aa35c6b", + "format": 1 + }, + { + "name": "plugins/modules/win_audit_policy_system.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d15d0a257629313b351bfe20dc3e3e53bfb9684f5d11200d67abec77c9629803", + "format": 1 + }, + { + "name": "plugins/modules/win_audit_rule.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2c554ab857f29e94beb0ba37ad2f4cd8ccfa44593d2cc4d0ed04271f7c493567", + "format": 1 + }, + { + "name": "plugins/modules/win_audit_rule.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "eff94970e25b87d3ab36d02597e31105e4e92f7e5f2363a4f5949e0feb8b6ed7", + "format": 1 + }, + { + "name": "plugins/modules/win_auto_logon.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "84aba3946501f41523f0e41c064b312178f101337e41bf99c4b555978de90215", + "format": 1 + }, + { + "name": "plugins/modules/win_auto_logon.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a71ef1214f1f1d6dac094a6b716360786e2e95d77ba91791897dd75c456b0de0", + "format": 1 + }, + { + "name": "plugins/modules/win_certificate_info.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "45d49459bd5d08804fa75594193e2caa088d34b414f6e0d839012b5630eec127", + "format": 1 + }, + { + "name": "plugins/modules/win_certificate_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "dae5c551c33a03234753bb764016e8956d8f094b6953664de02447396d7383be", + "format": 1 + }, + { + "name": "plugins/modules/win_computer_description.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1374ae3431a5126fd6d29f59cfe7261081b13873a852fabde843b4a96ea6f331", + "format": 1 + }, + { + "name": "plugins/modules/win_computer_description.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "16daf8c317e299551f1fcc3a5816f0d1479c5cc25d9f0adc04a3696946c48877", + "format": 1 + }, + { + "name": "plugins/modules/win_credential.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "80fa527239f1bfcfecdd6bf1c616f2d87c6189bd172d743738543a9e9cd0b321", + "format": 1 + }, + { + "name": "plugins/modules/win_credential.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "30a85aba06521091552b1fa44a137d7d988514ca81eeed77e86ade699d101e05", + "format": 1 + }, + { + "name": "plugins/modules/win_data_deduplication.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2c7aec5e19a625b7db6b3e4749ce4e315b901fa55995bf1770ded0fb8d60ac73", + "format": 1 + }, + { + "name": "plugins/modules/win_data_deduplication.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "da032fcac2beb3db04949a40f6977968d2190fd023620bc986aed1c5a0e3ee50", + "format": 1 + }, + { + "name": "plugins/modules/win_defrag.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d7a89a620602e7d81e05104ccc98faa442c32d926f84cebfcd104d7726f8fcb4", + "format": 1 + }, + { + "name": "plugins/modules/win_defrag.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c63ff75ed237af5f400ee7cb61cf9d43002bc6e2471a656912e2563be27add44", + "format": 1 + }, + { + "name": "plugins/modules/win_dhcp_lease.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a42338ea79ee526d8db794688916f4b68914d1fe26e3c3de4a12f5a8f6e0b16f", + "format": 1 + }, + { + "name": "plugins/modules/win_dhcp_lease.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "99386a3ea7fef0fb7d0ad9c3b974a6fafa20d4966a64c17ef209c9577e1ad1ac", + "format": 1 + }, + { + "name": "plugins/modules/win_disk_facts.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "13245b7038a2af9ec8209aed30b11556e56c60970b39836b075d9ca9df0f66fe", + "format": 1 + }, + { + "name": "plugins/modules/win_disk_facts.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fe0e52927b9e1b45ebe8b08c6b8a6ffc4196fcf07a0ba6d67f7f6eb7af581900", + "format": 1 + }, + { + "name": "plugins/modules/win_disk_image.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e6dab7499231082ecf1504560bcee982840f92fb7e215bfaabdc138ad87a2960", + "format": 1 + }, + { + "name": "plugins/modules/win_disk_image.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7e25e48f4a3c493889f11a86c2298ef52ac8df1e558912535023f7c41c1b8fec", + "format": 1 + }, + { + "name": "plugins/modules/win_dns_record.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1657585583bbc73e932b55cbe86945ae68c931170771430415024258d7cf984d", + "format": 1 + }, + { + "name": "plugins/modules/win_dns_record.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "10a9b6919bef02f4034f83dd03560f1b77aff0406f00150b2d36c87a71b19060", + "format": 1 + }, + { + "name": "plugins/modules/win_dns_zone.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "78b48f66e89e9e3096fb2cb4b7ce11407d12849311586c13db908121e67fa683", + "format": 1 + }, + { + "name": "plugins/modules/win_dns_zone.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8ac1ccc9d1bf3836cfc798b5439c92820d0b1f5c6e26d0a3b6552afb79e94b0b", + "format": 1 + }, + { + "name": "plugins/modules/win_domain_computer.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c43c1c29652398ef338e209849f1cf76c74ddfd673a4212201534df9132809ad", + "format": 1 + }, + { + "name": "plugins/modules/win_domain_computer.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "101d6fd2f2ef02c41c2cec63b4fa1ee8f460b11248c1dfc745c994a5d0ab9175", + "format": 1 + }, + { + "name": "plugins/modules/win_domain_group.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "07895cc5ceadb58597e57be7f5b5c61511534104af97517b1e5ac5875610f455", + "format": 1 + }, + { + "name": "plugins/modules/win_domain_group.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fb916eeb85dce7f787139f38710681470a7a8a840d9361d9221bc9214c463884", + "format": 1 + }, + { + "name": "plugins/modules/win_domain_group_membership.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d05021f165956acab8f09b2f014c5f680d33be460a698455b19a252437d1f82a", + "format": 1 + }, + { + "name": "plugins/modules/win_domain_group_membership.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "97fa2920c38801363c85556ea90f53b22f9ff066f54d9e5bcd3f06f0381616ac", + "format": 1 + }, + { + "name": "plugins/modules/win_domain_object_info.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "44acb4b85265d5aed4aac9f782d5d61f7fc24f052a6261838de95799cfca5ac2", + "format": 1 + }, + { + "name": "plugins/modules/win_domain_object_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "aa05994f622b5165b24eba67fcfdddede7b1f5cda26ac73a9f15eafbac2e9cbe", + "format": 1 + }, + { + "name": "plugins/modules/win_domain_ou.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ec0e5aea8a62bdbd5585e74d57d2be55368016381ff248864a7c6919b6225472", + "format": 1 + }, + { + "name": "plugins/modules/win_domain_ou.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4043273060a7c06c086008b2227f612aa36dfaffb261004419e80c8cbadbedd1", + "format": 1 + }, + { + "name": "plugins/modules/win_domain_user.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7fc16d0cd3f57e959ca22db8af5b6195f65dad04b2cef5ef7af663ea03c1aba9", + "format": 1 + }, + { + "name": "plugins/modules/win_domain_user.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ffe798e9c9350f6a1fc4289269c6693f6cc20128050e09d8fdcf74cbdeb9b1bd", + "format": 1 + }, + { + "name": "plugins/modules/win_dotnet_ngen.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "575040cac9fa0d06a736fd60b11c610855dbe48f0cd6b8276e26a21ce593cf18", + "format": 1 + }, + { + "name": "plugins/modules/win_dotnet_ngen.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e79e4ea31cb8328067991f3baa436d8ff91478ec7c8da1e209662299571d0d74", + "format": 1 + }, + { + "name": "plugins/modules/win_eventlog.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "687ed84b28849b5a3635a0677f131075773b8277a39a16c04a62273ccc992577", + "format": 1 + }, + { + "name": "plugins/modules/win_eventlog.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5dbf502792038c870c1b25db2d58594218669e19ab25d89bd4ffcfb51fefcffa", + "format": 1 + }, + { + "name": "plugins/modules/win_eventlog_entry.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0603c52dc048ad9cb794f85527a876dd5fb62b4ddf29a260226d73c4ba647c4f", + "format": 1 + }, + { + "name": "plugins/modules/win_eventlog_entry.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "eb7e39925162a3c37b79bde639c3e83281b817a2d5f53ea4095e42ce99a43d8f", + "format": 1 + }, + { + "name": "plugins/modules/win_feature_info.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b0fca41c13df917498946781fbd4395c4321739e4bfe72b3fd61049a14cd8af3", + "format": 1 + }, + { + "name": "plugins/modules/win_feature_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7bc2c00034e6999006133b8047e8007b0a2630bd86d62b73fe1fdfb4a04924a6", + "format": 1 + }, + { + "name": "plugins/modules/win_file_compression.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7588e945309e0bdb8a731ca9f66e1aec9cf45176a6f3f18b475015bf8ce0ba29", + "format": 1 + }, + { + "name": "plugins/modules/win_file_compression.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0df537162e2ce4117e6749dfd94411ca489f76d384dd621f484667b9e7496ac7", + "format": 1 + }, + { + "name": "plugins/modules/win_file_version.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1b95159ab4cd769bb5799640d4b796369b996f54e7be753d494270c432e1d65f", + "format": 1 + }, + { + "name": "plugins/modules/win_file_version.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2a08534f21873c2ddb628210b4afec447bbffaad47215cca794318120a8a898e", + "format": 1 + }, + { + "name": "plugins/modules/win_firewall.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e351fefbf51ee7b46b81c6790d7eca68d39ce58a2cccc84e5352e1ab2b9b51d4", + "format": 1 + }, + { + "name": "plugins/modules/win_firewall.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0bb66a28f99e8e1ca9244a66bed93deaf18ea81591f1279bf275c2ef5cc22390", + "format": 1 + }, + { + "name": "plugins/modules/win_firewall_rule.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ceef7482774115945c36c9f897d7f5af295989bd04d5375da5b3848e5758c8d7", + "format": 1 + }, + { + "name": "plugins/modules/win_firewall_rule.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d1cd72f48f051eae1cfc5af818de7636432153856649713587009239beb982b0", + "format": 1 + }, + { + "name": "plugins/modules/win_format.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f34249c8490c3805bdf9318ad5312716ad163663266b8e493e9af3f7d910bde0", + "format": 1 + }, + { + "name": "plugins/modules/win_format.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d671b30925e95eef7240039a7ef75e2d452e2a22cd93336b2aa4090cca3a2355", + "format": 1 + }, + { + "name": "plugins/modules/win_hosts.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1036b8649b2a06cfb667a466f9645e07f0138fd5bb32717dde29e767c54a4254", + "format": 1 + }, + { + "name": "plugins/modules/win_hosts.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8d5c7b2e50c45d2e4ee609a82ef46a8e15adff4aa98abdafb7f10f23294414b4", + "format": 1 + }, + { + "name": "plugins/modules/win_hotfix.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "110436fc4bba502c0377fe3fc969ad0041c175bd4dd07c22652d19710c678f31", + "format": 1 + }, + { + "name": "plugins/modules/win_hotfix.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "05080722a0991f4dab690e268e859da51905ff4064fd1eb609576a63a655b8ae", + "format": 1 + }, + { + "name": "plugins/modules/win_http_proxy.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c1ec23932813f5f2f142e559f1921d09bfd7149e4df8480ba0979db65ddbd898", + "format": 1 + }, + { + "name": "plugins/modules/win_http_proxy.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c1958a575a884e2fbb2a99d1a679cdc0f080dc16d3989603e8da521941bf85f7", + "format": 1 + }, + { + "name": "plugins/modules/win_iis_virtualdirectory.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d5dfc54ae80e012f1bbb6a90e3a2f81e666412079c2605ef113f9ecafdb3c48e", + "format": 1 + }, + { + "name": "plugins/modules/win_iis_virtualdirectory.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9646004cb33eb64be2f195027a399633078a3b242ffdd9725d25be8ae66492a9", + "format": 1 + }, + { + "name": "plugins/modules/win_iis_webapplication.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "43635ebe0d34651d13a76a86ec56fa734d349a97e0f76504d017adea1f56c115", + "format": 1 + }, + { + "name": "plugins/modules/win_iis_webapplication.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "282f42cf440cfe22dc73fd0f54aeb69d8820d28aa5a8f0b3a98f26afc309ee7e", + "format": 1 + }, + { + "name": "plugins/modules/win_iis_webapppool.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8a4a570124a3e6a29d86b178c55a998c7de96aa3b6792fe9ec7932488201e694", + "format": 1 + }, + { + "name": "plugins/modules/win_iis_webapppool.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4bbaa7ca07e530b819b95fd1ba1afa424373362d53a03cd7a1b386121926a29e", + "format": 1 + }, + { + "name": "plugins/modules/win_iis_webbinding.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "345e572661345211640ee961249336c86a21347d01568aa622a52a420207ab0c", + "format": 1 + }, + { + "name": "plugins/modules/win_iis_webbinding.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1ad14ed2f10fef63e47dcec4b45393c26a4804cf2d77e88c6a4e97074ced5826", + "format": 1 + }, + { + "name": "plugins/modules/win_iis_website.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "db6b98de531f0cd47f6bfdef85ba5c68df2f56cc919d80fcc1df545f5a711863", + "format": 1 + }, + { + "name": "plugins/modules/win_iis_website.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8f9b5da9abad43080caf8b3da8ba7b7ccc08ed472744dc5d631c9b76ea4800df", + "format": 1 + }, + { + "name": "plugins/modules/win_inet_proxy.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d347533fbe138a3c22b01a6543a098ff43d01a2fbd663e1744e7ffadbab570b6", + "format": 1 + }, + { + "name": "plugins/modules/win_inet_proxy.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "64e307bd3e6c35e6f9109a63f75965f99f35e521aa78ee365b033f088b746d61", + "format": 1 + }, + { + "name": "plugins/modules/win_initialize_disk.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "797284d0918c208697f93e273b656ba38bc2b9342f2b574b0dfbff68a183ca06", + "format": 1 + }, + { + "name": "plugins/modules/win_initialize_disk.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6482f73b96209bacec3660c86f549e1cc741c097836a256a0e6df32a6e1ba056", + "format": 1 + }, + { + "name": "plugins/modules/win_lineinfile.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "528af1f565bfd4a4f70f9fa87e8ead5da56bb224e31e4c50e0233767576f4723", + "format": 1 + }, + { + "name": "plugins/modules/win_lineinfile.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9cf7b6f00ceb8a5a05a36be419fbb36a44e270eb22d5e882c69f0ae6c347ff8d", + "format": 1 + }, + { + "name": "plugins/modules/win_listen_ports_facts.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "27037c5aa236a59514e3ac8b864ba5a1543ae593b50cd7fe0cab56724ef83ac1", + "format": 1 + }, + { + "name": "plugins/modules/win_listen_ports_facts.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "02126d246770675eb2cf569f0b95453a4da540121223ffdf9330f6e0eac4c2e5", + "format": 1 + }, + { + "name": "plugins/modules/win_mapped_drive.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5758175d8fbf5bb442624ddb44497b9325faacb7fff23e9967916df5b75db8d6", + "format": 1 + }, + { + "name": "plugins/modules/win_mapped_drive.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "122681bd66dda06e0de11eaed0793638892b342c9f4f924adb64f7800ce1a3a1", + "format": 1 + }, + { + "name": "plugins/modules/win_msg.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "82442977c4579b272e4713ef61c17d3d2ab5d61a87e5cf95db303c1f562c5f12", + "format": 1 + }, + { + "name": "plugins/modules/win_msg.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f1ca7f543f3adb170b757901fe94ff6b7f39f4596861d753c2b128415835590e", + "format": 1 + }, + { + "name": "plugins/modules/win_net_adapter_feature.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "da2015c98e5c97d7a901b782956ddfe7b13498c9968bb08142d9980cd028e0d1", + "format": 1 + }, + { + "name": "plugins/modules/win_net_adapter_feature.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3493bb9156842e8343ea91c7fe948d172956cb1222f1b547b98103b1a084063e", + "format": 1 + }, + { + "name": "plugins/modules/win_netbios.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9a9ccdb143548dd60fd122160b10851f683bf093157a4009b8c4f0c1828e998a", + "format": 1 + }, + { + "name": "plugins/modules/win_netbios.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f20f6f78fc8561e2eb92f87f376d915ca6465250d3957b09f3c6641bce742d64", + "format": 1 + }, + { + "name": "plugins/modules/win_nssm.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "40a69e606cb129c93644a67b1b18c49dd1cb1f93d28f51e3b5129189ebaa3c5a", + "format": 1 + }, + { + "name": "plugins/modules/win_nssm.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "03a2671f21160afbe8065cfb359a397683403edffe889099f78751f4dc7fc236", + "format": 1 + }, + { + "name": "plugins/modules/win_pagefile.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bfd77669240e024744d4808b371d2483c7811e18fd0a1899262200a81f95e09d", + "format": 1 + }, + { + "name": "plugins/modules/win_pagefile.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "79ce5da00a9acd46d9c49405a8062620b2ec21c4a95f6c66c750cefc8a63151a", + "format": 1 + }, + { + "name": "plugins/modules/win_partition.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f74e3a9f1e375f074bf162a20f3a0724f7e18d78ba5730c555b047112c4dceb9", + "format": 1 + }, + { + "name": "plugins/modules/win_partition.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "27a2cf0c472062bab2d3164fba17ece5673981e353b7bdaa21cb2b750bd3b90e", + "format": 1 + }, + { + "name": "plugins/modules/win_pester.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3d57f926634d2a7a58f0eb07f185f35b414d879a8a7d6cdfcab5341de2557bc8", + "format": 1 + }, + { + "name": "plugins/modules/win_pester.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "91a178ac5e9e31243466b9c5e9b2900946726b780c6b6d5194dbc12fa3f07c2e", + "format": 1 + }, + { + "name": "plugins/modules/win_power_plan.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cb77fc811fe9cf9b6681b3e25d941bfe125599b3735d675bd295e71544ac11f2", + "format": 1 + }, + { + "name": "plugins/modules/win_power_plan.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b604d390f2e1f6619071e69ac70305806f7d8912d23546443120d6fcf9326e71", + "format": 1 + }, + { + "name": "plugins/modules/win_product_facts.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d1c0a1b92f999d722bed52c100beadcd46438671c97cc2b1c3c21d4f2aa2eeb9", + "format": 1 + }, + { + "name": "plugins/modules/win_product_facts.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1bf7dc30b23c65a971865e4b254e8d1c4a14e3c578c2b9a6a08d523d230ea057", + "format": 1 + }, + { + "name": "plugins/modules/win_psexec.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7db20eee19a35e5fa5a31d1ce3d6f189434ded006ee4ea59b44d42a554c0ad86", + "format": 1 + }, + { + "name": "plugins/modules/win_psexec.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1e5b8e26d60970759907a9a8cc0842ac6b52c85ac9ac37af2fb56f487ab5877b", + "format": 1 + }, + { + "name": "plugins/modules/win_psmodule.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "892a75069e7880102b5816ff654fcc0d730e3d04d5fc9ae139e1b6ed57af4435", + "format": 1 + }, + { + "name": "plugins/modules/win_psmodule.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "133ad88c1854ddfabc775e9bd0c3298f294cbe17170a1374bee0d38011dd5df9", + "format": 1 + }, + { + "name": "plugins/modules/win_psmodule_info.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "59f0097dbee173d19af29d9e403d1979cbe67ee82dd3983b561743f3d9faf98a", + "format": 1 + }, + { + "name": "plugins/modules/win_psmodule_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "29e8a0936aa0c14e0a2ef9442eb72c33472b84beb96dae530781ac4c01c198b4", + "format": 1 + }, + { + "name": "plugins/modules/win_psrepository.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c8ad335436f53e34cc3b2459a1efb59724f64b28d1f77b8335e510c9295a9723", + "format": 1 + }, + { + "name": "plugins/modules/win_psrepository.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ed864327a9cd640fed80081257343c1cb66af7e8bc0b260e67e75597bd796b5f", + "format": 1 + }, + { + "name": "plugins/modules/win_psrepository_copy.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "04ae8ac1ac2623d74deca09a45200533b57870e4f7f79632ba46eed8de107fcf", + "format": 1 + }, + { + "name": "plugins/modules/win_psrepository_copy.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d2ffb7e29c5ed11862b4c536af70d6f212ce434ad364342ce0b8c029ec624971", + "format": 1 + }, + { + "name": "plugins/modules/win_psrepository_info.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "98a8d63ea5f55e04b4e39067ebf87fdf5a8494055d2f420ffaffcf48d324ae84", + "format": 1 + }, + { + "name": "plugins/modules/win_psrepository_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "efa35baf89e0e88b97973070d92f0f47c97b9419f8bd58245e00e33d0ad00ab9", + "format": 1 + }, + { + "name": "plugins/modules/win_psscript.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fbf87422951563237f07cf61661e546d0cad91ae8368f773549922f47e8bae94", + "format": 1 + }, + { + "name": "plugins/modules/win_psscript.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "843227311887bf2aeccf92d4669a6546f7f3326b7c4b2ae0ff3f84eb42571de1", + "format": 1 + }, + { + "name": "plugins/modules/win_psscript_info.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "945512e308cbcfba5f28ca5f417643784f337633a332b7e33006ebfcb2e97db8", + "format": 1 + }, + { + "name": "plugins/modules/win_psscript_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bcfb73e1fe40d2afcfe4eba1c784a66f93d3fb1f34ddaf440ec4b8d920a0c7e0", + "format": 1 + }, + { + "name": "plugins/modules/win_pssession_configuration.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1c521cf7056fd6caa24ffc86ae9496f0a53f47f0f6b9703e5f0774ea0c3f4b6a", + "format": 1 + }, + { + "name": "plugins/modules/win_pssession_configuration.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "672b93086ed2a49a66f9b9b816bc7e721c00bb34103c24885be779bb0a93784a", + "format": 1 + }, + { + "name": "plugins/modules/win_rabbitmq_plugin.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "07addf969c9dd2aa3976166d666088f9b2ae983c14d9b093f11df73e85427fcc", + "format": 1 + }, + { + "name": "plugins/modules/win_rabbitmq_plugin.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "aa62213b0b80ad9af291befd427c3fdfd41d77c65d1ad6a3baf5edab00d3b42f", + "format": 1 + }, + { + "name": "plugins/modules/win_rds_cap.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6ed5194192f186289c286f4f59aaef3f42cda3845919abffb16f04ac83a417ea", + "format": 1 + }, + { + "name": "plugins/modules/win_rds_cap.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6cb4179fe8df5a5476421f1ab4fa848cf0edc4a2f893104190478def3f69cbe3", + "format": 1 + }, + { + "name": "plugins/modules/win_rds_rap.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a68868e66f66e4a61770a9ee796e99e8ebbe357115f84ad4da828c64526d2227", + "format": 1 + }, + { + "name": "plugins/modules/win_rds_rap.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "beeeae76c33588bf55c7ba3f9fb9da372475da741b39ca1c9a85a631606d0afa", + "format": 1 + }, + { + "name": "plugins/modules/win_rds_settings.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "baa24177579313f433c67c727ece10727073dc149c4c67a4c853ce3df3637e1f", + "format": 1 + }, + { + "name": "plugins/modules/win_rds_settings.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "64c9cd29d9657b908b50b1ecd865ff3919ea36737da4d2c34691549e8b5220a5", + "format": 1 + }, + { + "name": "plugins/modules/win_region.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "dc30a601eb36f93f8eebe60b3c72fe465dd5ce398158592c3987188009348b01", + "format": 1 + }, + { + "name": "plugins/modules/win_region.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7be1246dc3b629b77afb3db6020240e7fc4796a160d51d3c5198934fc9c31d42", + "format": 1 + }, + { + "name": "plugins/modules/win_regmerge.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4f2dbcc0eb79462e852a0c5fd980b8648a5afadbbb10340e8360281e30daadbf", + "format": 1 + }, + { + "name": "plugins/modules/win_regmerge.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "681d727dab3d657faa5066afdf8297e0a7aeb7a2541ce671d31ae155b7e7999b", + "format": 1 + }, + { + "name": "plugins/modules/win_robocopy.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9598922f7696810f4a460223401cb9b068095bb93fc2b8a75729b80328c1ec45", + "format": 1 + }, + { + "name": "plugins/modules/win_robocopy.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "17bd16955f73fd5ced67813a347fee276fd2f667d1aa155334a07f0708fa9f3f", + "format": 1 + }, + { + "name": "plugins/modules/win_route.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "838932e97cd1e4addc869c3b1dd8df5088521100a2dd774739b6b31999807883", + "format": 1 + }, + { + "name": "plugins/modules/win_route.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f3a17857e18a5e2f81bd020f60cc71f2e322c561d92bc2665e78bee3067948eb", + "format": 1 + }, + { + "name": "plugins/modules/win_say.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1dfec9e2527c4f2250080076615a85b71feb64fd2ec6039d44f399b439b54ee7", + "format": 1 + }, + { + "name": "plugins/modules/win_say.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "58df81350081b2385fd3e6c742cbde796126fd7ecbeba88fea51cbef1540d39c", + "format": 1 + }, + { + "name": "plugins/modules/win_scheduled_task.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3e96402641385a6f49b4de9570cb1c389015fe32dbdb111006ed9e9e5d5a3036", + "format": 1 + }, + { + "name": "plugins/modules/win_scheduled_task.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7d32f24beef2ff2a8f7167f0b4db1e5423e4cbd064fb8a2f0b18d4bc5e383e95", + "format": 1 + }, + { + "name": "plugins/modules/win_scheduled_task_stat.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f490d3f7fa6388ebd04eb303ee822de1cdce1e405dc280c2e7d62e14ca3e2127", + "format": 1 + }, + { + "name": "plugins/modules/win_scheduled_task_stat.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f4138decb47384221b6f9e8a180a54c1e0c62d4fe638824f0bca481a44016118", + "format": 1 + }, + { + "name": "plugins/modules/win_scoop.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c3524243333a2ca41c7c80c1e159cedfd16b25782d1883ab222156a7d0bed7b1", + "format": 1 + }, + { + "name": "plugins/modules/win_scoop.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "37fb7a4b32c71fa655b81b6fddab4e7678e0b3e1d22ffbfa0a995641b7e2a216", + "format": 1 + }, + { + "name": "plugins/modules/win_scoop_bucket.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3b0cc33d792d47a4c05b7713329ed14e7f544e406651d2cce563455f2bc8c4b0", + "format": 1 + }, + { + "name": "plugins/modules/win_scoop_bucket.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9301e60a9678088187977b9488da4847c8ab735cd0af2efc8a70c75ea25e418e", + "format": 1 + }, + { + "name": "plugins/modules/win_security_policy.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2f91c88a8f685e1e92bab981b04024d12a1ce1b2215749496dc435ea8792a28d", + "format": 1 + }, + { + "name": "plugins/modules/win_security_policy.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c60e13b8be895948a8197017214f4da09dceaee71343e4b5b222ad42497c4f95", + "format": 1 + }, + { + "name": "plugins/modules/win_shortcut.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4702c7b2f83ae87ba102564ba40c931f42566df13530d4065c12f78131c903d7", + "format": 1 + }, + { + "name": "plugins/modules/win_shortcut.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "822eee0293b7f729908e218ff2623d6ed3f6f72d9ae6418bc8566cd881485e83", + "format": 1 + }, + { + "name": "plugins/modules/win_snmp.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d4b8333c9e552cb8d40b293cf40350381c5a777b1451c5ffc82a0dba8bc2112d", + "format": 1 + }, + { + "name": "plugins/modules/win_snmp.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cf13620444cfbd22c2fc7d53d18dd9030d3884d0c65076b392aa74bd50186bde", + "format": 1 + }, + { + "name": "plugins/modules/win_timezone.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b8fea40a135430924a9590d8548db8b424f8edd0d8ae603c279d36a1159fd123", + "format": 1 + }, + { + "name": "plugins/modules/win_timezone.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "08340e5bddef98762dc1b0f6ae148770ce7466fcc580197a1d3509aa8a50ad1b", + "format": 1 + }, + { + "name": "plugins/modules/win_toast.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "33294de18e8987a8597467ad820ea86149e0d46bbc15111668053be4791a04e7", + "format": 1 + }, + { + "name": "plugins/modules/win_toast.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eae86e42a8d8fc3d48688214f2a7c1b3d023ee32e8f1eae7d5f0b5366bef27d", + "format": 1 + }, + { + "name": "plugins/modules/win_unzip.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "38e9febc3e910631bc358a73cb08957f5695333cd96986e409dd0e86f5d54e3b", + "format": 1 + }, + { + "name": "plugins/modules/win_unzip.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a7bd6244817abe099f70e6ad1d4f801bced3c0c197c385017b2831800636f393", + "format": 1 + }, + { + "name": "plugins/modules/win_user_profile.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "84780105410699b87cad0b6047e5aca82ffee2f55bd6a3cf99c8ec25fdddb76a", + "format": 1 + }, + { + "name": "plugins/modules/win_user_profile.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ae604375d97a26650fdf79fce7c373ed689e82e2695106a49e325b6ba64e126f", + "format": 1 + }, + { + "name": "plugins/modules/win_wait_for_process.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e8427d9451ef01f633c8fc9d3d80b70af34c3142af862b7c75c61f0cb21b4664", + "format": 1 + }, + { + "name": "plugins/modules/win_wait_for_process.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "20db259f380c06193f19545c1b6ba37f51496f6016442a1d684f9dbb02f700b4", + "format": 1 + }, + { + "name": "plugins/modules/win_wakeonlan.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "474da7897b5606cf41061d4274952b9320bbf57d45418ecbe6e53c3a6c34aa85", + "format": 1 + }, + { + "name": "plugins/modules/win_wakeonlan.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ee9439ec381a284965803e1eb7801ac08ac8bb24c88b76ac6699d241a478ffe3", + "format": 1 + }, + { + "name": "plugins/modules/win_webpicmd.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "36f22bcd5c1462cee9a8ac0bbce14332d6594da183194b9a70cdb3935b8bee37", + "format": 1 + }, + { + "name": "plugins/modules/win_webpicmd.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7cc1598a0cbc5004e25c7eabd58e9018335a63fd992c4c90386c018bf7336793", + "format": 1 + }, + { + "name": "plugins/modules/win_xml.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "985d263d1a35b468550254d2fb97a813ad618da6b682f75b0251149e566e0843", + "format": 1 + }, + { + "name": "plugins/modules/win_xml.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "24691cc754275cfa59e5b83433ade700408ad4aa254a2553a87b55d38d74f5ab", + "format": 1 + }, + { + "name": "plugins/modules/win_zip.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "99fefe0ee51e179ab089da3b08fe537a36e9b554ac339930cff0b954cacd0957", + "format": 1 + }, + { + "name": "plugins/modules/win_zip.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5801a8fd55fd31d2b14b79e9db0798c0529670878b3c98b3ca20ba8ff7377be1", + "format": 1 + }, + { + "name": "tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/psexec", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/psexec/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/psexec/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0077cab2153ab89820016f8f1e9c0f84635b873e064bb4f20d29071c8af3ebdb", + "format": 1 + }, + { + "name": "tests/integration/targets/psexec/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "dcbe4af550accc8de678a6b735b5b216271bc5af024a4763354f876935e6d88b", + "format": 1 + }, + { + "name": "tests/integration/targets/psexec/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "00d4cd3047049c7eb0a2b3438d5aef7cd6801ecdb2acdc2db636c3b3e633655e", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_domain_tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_domain_tests/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_domain_tests/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0c700ec78b967e79db181172a78af8ea185194891aff6a81309340620d280fb2", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_http_tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_http_tests/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_http_tests/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7d57ce61e6260a2323345c4a0d31313443d983e2c8c53f6b2f54cf4f3c6a3acf", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_http_tests/handlers", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_http_tests/handlers/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a2917674eb617cb07405eabb6e384f00655f265ea067c59113bd10011c5b0d46", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_http_tests/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_http_tests/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1e8d632f9db7209967c5b2f6d734bede09841acc7b898dafc19f31c72cee9929", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_http_tests/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_http_tests/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "47a062c0c69fbce36f3c25dfb63c900b7ff575bd730f3ac90152f6bd2fa06b6f", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_http_tests/vars", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_http_tests/vars/httptester.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ee7232593b60212e0e400015c9d479c5f5193f14c8868b044ceffceb7a54196a", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_remote_tmp_dir", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_remote_tmp_dir/handlers", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_remote_tmp_dir/handlers/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6d772766d4d6bba55f757450a7024c5c7b63a85a2307ab6f12f3a159898e65f3", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_remote_tmp_dir/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_remote_tmp_dir/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b015a5c462afb0dde4020c03cb9f4e1ee3e67a8e006dba73bf12d88001799dd1", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_win_device", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_win_device/handlers", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_win_device/handlers/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "def30fca76ea4848d7411b1e0d6ad066d900fa35d8df447b5770bc56cd3a69f6", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_win_device/library", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_win_device/library/win_device.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ebab88c2d4b9e18e34416b0c2950c53667ac82f3541ddce608336a3bfedfe016", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_win_device/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_win_device/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "26290fb53d3f17e21eec6cdd02a33bf8e31d0f3257ffd209c87c3a62a81f2bd7", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_win_psget", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_win_psget/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_win_psget/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "92f2b42912355ba7fd19805552d4e3b15a5f55ecc473890e37a6ef48c7886b33", + "format": 1 + }, + { + "name": "tests/integration/targets/setup_win_psget/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/setup_win_psget/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1dcfbb5373a44c3cbf9c1dd98d6d608aa31972f3737dd8be9d50e0edf46f401d", + "format": 1 + }, + { + "name": "tests/integration/targets/win_audit_policy_system", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_audit_policy_system/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_audit_policy_system/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "101bf03ac6f0065687588431935c9892b225263ce54f65fbc322affb9d4a69e8", + "format": 1 + }, + { + "name": "tests/integration/targets/win_audit_policy_system/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_audit_policy_system/tasks/add.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e4218ded5fabbd048fb7aea0d60bafb9a46fe81a2532ebcc58c44ab7fb27881b", + "format": 1 + }, + { + "name": "tests/integration/targets/win_audit_policy_system/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "92e4fc00ef836c06e55a6e790c57dcdc350176db33fa0efae6fd84bf078f6ca9", + "format": 1 + }, + { + "name": "tests/integration/targets/win_audit_policy_system/tasks/remove.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "00d19ae6e926cadbf4ab9d1115078cb43904ef7ca5b8a0ed5b5fabf25eeef218", + "format": 1 + }, + { + "name": "tests/integration/targets/win_audit_policy_system/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "758d23e7651820af7b1af35846b0a80daee9e3c321fd9e93a0f60b929035610f", + "format": 1 + }, + { + "name": "tests/integration/targets/win_audit_rule", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_audit_rule/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_audit_rule/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "72042a27b803a3e1c40e82fb71662e3956ce9e2b3d2a0a26f4a90504cfc22a8c", + "format": 1 + }, + { + "name": "tests/integration/targets/win_audit_rule/library", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_audit_rule/library/test_get_audit_rule.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "950e0feb2ffb782733e73110d4b97461351c78795fd8e3f08f221bda9174dbc1", + "format": 1 + }, + { + "name": "tests/integration/targets/win_audit_rule/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_audit_rule/tasks/add.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a4cf5962a9c337235ebebc0277ba3be0bf364e24101b252b81d5c8947b155251", + "format": 1 + }, + { + "name": "tests/integration/targets/win_audit_rule/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f47ead88852321dcfb1aba6a1587f696213279b38bd01a9fa5453ab07b6558ff", + "format": 1 + }, + { + "name": "tests/integration/targets/win_audit_rule/tasks/modify.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9835ba1d30c264804b3898217be61581f6d532aae3835c3c73a5af53a913ff65", + "format": 1 + }, + { + "name": "tests/integration/targets/win_audit_rule/tasks/remove.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bccd47fd367a688ef824d1ad61a12f59763b7f04d420775dd8969f5f4d5bd14f", + "format": 1 + }, + { + "name": "tests/integration/targets/win_audit_rule/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1aabf18b3e493a2bf65e8256494940c15795202c0132ef951482454ec995d2da", + "format": 1 + }, + { + "name": "tests/integration/targets/win_auto_logon", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_auto_logon/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_auto_logon/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0b6a8671fe6d067fcf96c9fc3f6fc3cfa59d6c67de0dac57692bdcb1fec92230", + "format": 1 + }, + { + "name": "tests/integration/targets/win_auto_logon/library", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_auto_logon/library/test_autologon_info.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5ee3deabbadcd11357c7215a8d8b61967d58fae1541e06fd7f6a6f06a6acc88f", + "format": 1 + }, + { + "name": "tests/integration/targets/win_auto_logon/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_auto_logon/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "74982000b79c44045e36f6d053fe939357f9303cc0293909ee5b5bcfcd54b42c", + "format": 1 + }, + { + "name": "tests/integration/targets/win_auto_logon/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fcac39fe572969e883ebb9bdf5630be559fbfbb1ee87fe89e12b3399a82eea89", + "format": 1 + }, + { + "name": "tests/integration/targets/win_auto_logon/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1afcc2aeb0a4a4786e8345e0738aef8df6eaee36d40e45e0d0e54a500de582d4", + "format": 1 + }, + { + "name": "tests/integration/targets/win_certificate_info", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_certificate_info/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_certificate_info/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "38db639a3622ff7a4b711ddb13191173236c5660e807a633267b7e408ad04bf2", + "format": 1 + }, + { + "name": "tests/integration/targets/win_certificate_info/files", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_certificate_info/files/root-cert.pem", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a8b3f2ac0a48884f52d66c53b4215b93c3750127763ec5a67af996199983c8a0", + "format": 1 + }, + { + "name": "tests/integration/targets/win_certificate_info/files/subj-cert.pem", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "96029db5647ebb67fa83ffa98944469e2f03e422a785a87cdfcf12c136e61469", + "format": 1 + }, + { + "name": "tests/integration/targets/win_certificate_info/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_certificate_info/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7df6cd7eb977d8bebdc11bf664ed83e01fd821f94cbeae22309fca5a38014700", + "format": 1 + }, + { + "name": "tests/integration/targets/win_certificate_info/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_certificate_info/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "47baa40987a16fabb0e569e782e2d603c316f57a952aca0ca868995013bcdaea", + "format": 1 + }, + { + "name": "tests/integration/targets/win_certificate_info/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6da965f79e29128b52a75bea075f125f29a04eca5f5a72ee37f982541dfbcb06", + "format": 1 + }, + { + "name": "tests/integration/targets/win_certificate_info/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "758d23e7651820af7b1af35846b0a80daee9e3c321fd9e93a0f60b929035610f", + "format": 1 + }, + { + "name": "tests/integration/targets/win_computer_description", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_computer_description/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_computer_description/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1d9e0ad95788e69d14a6434b56faa9ee4689ad15e093b1326af7327c82108b10", + "format": 1 + }, + { + "name": "tests/integration/targets/win_computer_description/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_computer_description/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c23f60f1fd0002abe3964be65af3be3daa6a3fc1e55ede9741145ed8b037d104", + "format": 1 + }, + { + "name": "tests/integration/targets/win_computer_description/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1aabf18b3e493a2bf65e8256494940c15795202c0132ef951482454ec995d2da", + "format": 1 + }, + { + "name": "tests/integration/targets/win_credential", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_credential/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_credential/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "227ec4a7409de803ce17b61e9808a0a0c7c30c63e75da78bde64c160b06a39c9", + "format": 1 + }, + { + "name": "tests/integration/targets/win_credential/files", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_credential/files/cert.pfx", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1d16a5af6e33e07601e46cb928155c338086fa13fa8468c38417b6fbe3ccc347", + "format": 1 + }, + { + "name": "tests/integration/targets/win_credential/library", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_credential/library/test_cred_facts.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "30545f69012f834c56b0bacfe121e2ea81d14f3fb513b1ff9de9401c8c0c2646", + "format": 1 + }, + { + "name": "tests/integration/targets/win_credential/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_credential/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7df6cd7eb977d8bebdc11bf664ed83e01fd821f94cbeae22309fca5a38014700", + "format": 1 + }, + { + "name": "tests/integration/targets/win_credential/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_credential/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a2035deb95def502403afadaf63d205463906957806b6fa90335b21dba56d217", + "format": 1 + }, + { + "name": "tests/integration/targets/win_credential/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2585b41547cab1819868518706f6a51f0fa3ad4020d812348d72816ef786ff1f", + "format": 1 + }, + { + "name": "tests/integration/targets/win_credential/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1aabf18b3e493a2bf65e8256494940c15795202c0132ef951482454ec995d2da", + "format": 1 + }, + { + "name": "tests/integration/targets/win_data_deduplication", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_data_deduplication/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_data_deduplication/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7df6cd7eb977d8bebdc11bf664ed83e01fd821f94cbeae22309fca5a38014700", + "format": 1 + }, + { + "name": "tests/integration/targets/win_data_deduplication/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_data_deduplication/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "11015c09652cce419325b63f7e5a8a2243ee87d5174a119d569c51a774776990", + "format": 1 + }, + { + "name": "tests/integration/targets/win_data_deduplication/tasks/pre_test.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a0005d284731b397c14fce162751f7df806f1ce3bde97f9e5af79dd46412d991", + "format": 1 + }, + { + "name": "tests/integration/targets/win_data_deduplication/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d16192acca0e6ae9b3e44d3eaddd0bb1ebff18b67b4f50eff3687ea2df30f37f", + "format": 1 + }, + { + "name": "tests/integration/targets/win_data_deduplication/templates", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_data_deduplication/templates/partition_creation_script.j2", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c06015e67cdb473a313de91d22f60d6e9aa1109d2b4ce79ce5b412b154d0d87d", + "format": 1 + }, + { + "name": "tests/integration/targets/win_data_deduplication/templates/partition_deletion_script.j2", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f1319734e0deb3e19ae8c1f4c36557cf4a2c53189bdff2ac88fe315af8108877", + "format": 1 + }, + { + "name": "tests/integration/targets/win_data_deduplication/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "73346dc8234e120681840b8d2642d4ebd05e1d25db50cb2cbe9376e5632528c8", + "format": 1 + }, + { + "name": "tests/integration/targets/win_dhcp_lease", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_dhcp_lease/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_dhcp_lease/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "91065c6ded906640b4603afd9b3da0254763a1131ab86bdb123456c0a3a85a45", + "format": 1 + }, + { + "name": "tests/integration/targets/win_dhcp_lease/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_dhcp_lease/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3b352b2db2ac02887606efce13ce5b1dd5f4058ede848b31aba1821d2b6e296d", + "format": 1 + }, + { + "name": "tests/integration/targets/win_dhcp_lease/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e08e0e49f2849aea5a28566739d1447dba1d8c2c81226f84df698439dcbd3b60", + "format": 1 + }, + { + "name": "tests/integration/targets/win_dhcp_lease/tasks/tests_checkmode.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c7f8bfcea6158a55c73374b3eaddbd4f139416de2380ba28d357408aa1dfa58f", + "format": 1 + }, + { + "name": "tests/integration/targets/win_dhcp_lease/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1afcc2aeb0a4a4786e8345e0738aef8df6eaee36d40e45e0d0e54a500de582d4", + "format": 1 + }, + { + "name": "tests/integration/targets/win_disk_facts", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_disk_facts/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_disk_facts/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "25d09891b2275f91de4d285b10e8d93437e1be449691bfa328875b963e841084", + "format": 1 + }, + { + "name": "tests/integration/targets/win_disk_facts/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3a64ac787e90c003dd146d230144a54355bfcbd015234c7e157cb1140e5e0b6", + "format": 1 + }, + { + "name": "tests/integration/targets/win_disk_facts/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1341fbf9f0e0665f9960fa59505be4ff8ccb45aedd059aa843f1845b5111f72d", + "format": 1 + }, + { + "name": "tests/integration/targets/win_dns_record", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_dns_record/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_dns_record/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6ed5712c4b274e25a4ed5f32d35b0415d1dbf446963a0cd7a6f1d9ecc5ef3edf", + "format": 1 + }, + { + "name": "tests/integration/targets/win_dns_record/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_dns_record/tasks/clean.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b40cdd701e5c463d24de51b88511b745c4684c02bdc489e1982f0a977461a00f", + "format": 1 + }, + { + "name": "tests/integration/targets/win_dns_record/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f9cee2e389f87db84fe355d51e7517db5737663e1373765c2ff5b923eaf19e92", + "format": 1 + }, + { + "name": "tests/integration/targets/win_dns_record/tasks/tests-A.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7aaac77e07497101439dc313fc4e5160514cc46227817b4a110b835d51eaef02", + "format": 1 + }, + { + "name": "tests/integration/targets/win_dns_record/tasks/tests-AAAA.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "271e12e3191ae9ef4f9113db59cf67a6b2b42e9a35d2419452cb2b66e4c2d8dc", + "format": 1 + }, + { + "name": "tests/integration/targets/win_dns_record/tasks/tests-CNAME.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e264d93c8acbd465b38b462486b35212f5efcf3fe9e39067d6f332672f1f9063", + "format": 1 + }, + { + "name": "tests/integration/targets/win_dns_record/tasks/tests-DHCID.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8b9e14d7f48eaec7f3a2519c5d8f2fd412f8e5f9c592694339ad1e4cb5335a65", + "format": 1 + }, + { + "name": "tests/integration/targets/win_dns_record/tasks/tests-NS.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ef7ec9a961620244edceac4f9e6daf2198b046a7f648756cd2eba7ceddc5f431", + "format": 1 + }, + { + "name": "tests/integration/targets/win_dns_record/tasks/tests-PTR.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1ac1a8b41d892689ae70da98efe9f70d7341080c0374a102094d33922973be5e", + "format": 1 + }, + { + "name": "tests/integration/targets/win_dns_record/tasks/tests-SRV.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d5f32c30f47ac4d9bf6d261f98d04ab85381febd6866478e11bf3ff2db189b3c", + "format": 1 + }, + { + "name": "tests/integration/targets/win_dns_record/tasks/tests-TXT.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b63307a382d526f80d6fc0fff6177eca4479f38e1e5c090f3259e6e62dade431", + "format": 1 + }, + { + "name": "tests/integration/targets/win_dns_record/tasks/tests-diff.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2bf0f609051efcf949aa6e26f11eaf72b259f06fc292ea41363782f6fcd12fed", + "format": 1 + }, + { + "name": "tests/integration/targets/win_dns_record/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "675088d5e1095c8b20552e87147375e532e1faed5ae033d2363bac62fd5c66ba", + "format": 1 + }, + { + "name": "tests/integration/targets/win_dns_record/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1afcc2aeb0a4a4786e8345e0738aef8df6eaee36d40e45e0d0e54a500de582d4", + "format": 1 + }, + { + "name": "tests/integration/targets/win_dns_zone", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_dns_zone/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_dns_zone/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1dcf2811ba9d1100742489e47b88ffc23cfd318b5e8b880196aa6b9aebc63de8", + "format": 1 + }, + { + "name": "tests/integration/targets/win_dns_zone/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_dns_zone/tasks/activedirectory.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f9f0570e0f3e4e6869352516b3eaeada90c632ead943b6910a7b714bf488a3b4", + "format": 1 + }, + { + "name": "tests/integration/targets/win_dns_zone/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "50554ec70e9ab2007324603fd7734069ea3166c0cc8ef76bbe62927092ece109", + "format": 1 + }, + { + "name": "tests/integration/targets/win_dns_zone/tasks/standalone.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4d7909e714f262cb2c1524a7ac8162614b2d3980b7fc824e5fb05f6219410d48", + "format": 1 + }, + { + "name": "tests/integration/targets/win_dns_zone/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8ba362a47e99d88d85643228ff2a522e7e4c1496b38e09d024fa05b20b993542", + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_computer", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_computer/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_computer/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8d8e73237ea2fae1bb5637b8e28b02c308d1fde840f7def2c8b822b930f1c158", + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_computer/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f13674351f8231c9e60fc923652b1ce94b62357ee00fedaf23ad3af677aff656", + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_group", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_group/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_group/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1601efef3a16283d77f1996ac2245cc17e06c4463dae088921e945d72c6b090a", + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_group/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_group/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "db67c3178ea4d99d6fd0aea65325d3f238fa9ba772b54899bc191eb4c3546c7e", + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_group/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f13674351f8231c9e60fc923652b1ce94b62357ee00fedaf23ad3af677aff656", + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_object_info", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_object_info/handlers", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_object_info/handlers/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e0d33f3cfde0a5bcfbc71bc02b66ed8d9d45725ce22725882716a7892cebc50d", + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_object_info/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_object_info/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0e92b1fa666c75042c50e2cfbd27cb55b27d70abe20c241ba8995808be8a7a47", + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_object_info/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f13674351f8231c9e60fc923652b1ce94b62357ee00fedaf23ad3af677aff656", + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_ou", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_ou/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_ou/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7713bb184c50244e00adbe5f8860f2d075df8aabc99c8546b03c31f07f892fce", + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_ou/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_ou/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ec59f29dc11868d661c7240c49a207705a01cbd947ad4eaf3b0467edf05f40ea", + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_ou/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_ou/tasks/check_mode_test.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "95cea8ccd4466d785d20a50fff0e058d6efd1ac2dcc1dab2a535465bcb7827a6", + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_ou/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9188fec33517e2e33115494f06afd9847ca31d2256c33720ba52f3fbfe8f224d", + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_ou/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7a64b5cccfb83f7a81a6482edf70066ed41e92fb94ba19c6f7f9b31e609c9bdf", + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_ou/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "247913365e3e278d856d0a3264801f680390008f151e1824e53419c04471bb0e", + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_user", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_user/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_user/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ec59f29dc11868d661c7240c49a207705a01cbd947ad4eaf3b0467edf05f40ea", + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_user/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_user/tasks/check_mode_test.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3c08f1346b48b64276e3229fe2b97f3e20637d99cdbfde844571d1fcafa0e0d6", + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_user/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7203dd2321956a1ce84e12ac992df09363b244ea2bb28a1453b22313bcb6bdbb", + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_user/tasks/test1.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "261750bb907f36ff416a9a9439fca0320539e444832f2446437c9e085b785a25", + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_user/tasks/test2.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "00d59acceb1a64b0ba7728ab174b7ed76fa62c35cb85c1a41c3125cb4c213839", + "format": 1 + }, + { + "name": "tests/integration/targets/win_domain_user/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "247913365e3e278d856d0a3264801f680390008f151e1824e53419c04471bb0e", + "format": 1 + }, + { + "name": "tests/integration/targets/win_dotnet_ngen", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_dotnet_ngen/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_dotnet_ngen/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1055e179dd53666d89d7c5bcca5a63bebc8ce866e414273aadc7b836b692c492", + "format": 1 + }, + { + "name": "tests/integration/targets/win_dotnet_ngen/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "758d23e7651820af7b1af35846b0a80daee9e3c321fd9e93a0f60b929035610f", + "format": 1 + }, + { + "name": "tests/integration/targets/win_eventlog", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_eventlog/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_eventlog/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "61119c6af04d17396015d13375a74cbd7db87a446d501df2724d8072d07d3122", + "format": 1 + }, + { + "name": "tests/integration/targets/win_eventlog/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "28354537e8963ea52470f928ac82fad9bd1e8c77d6d08a079483f5cf3e9a4ebb", + "format": 1 + }, + { + "name": "tests/integration/targets/win_eventlog/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d7127ace6c1571fdf343a57b543cd35ed5b5181a330ab6e79ea892bf1ffff082", + "format": 1 + }, + { + "name": "tests/integration/targets/win_eventlog_entry", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_eventlog_entry/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_eventlog_entry/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3ee2224016a3c9f6cae6b003482f55995e88dc8dfffeb71f516da177d490d91c", + "format": 1 + }, + { + "name": "tests/integration/targets/win_eventlog_entry/library", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_eventlog_entry/library/test_win_eventlog_entry.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "aecdb7299a53de0eb112057280c39baba49920585fbcc6e4c872a1fcf65eb268", + "format": 1 + }, + { + "name": "tests/integration/targets/win_eventlog_entry/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_eventlog_entry/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "366a96b2c24c798698692d1a40f16434f9973c8bfb8fc95503d75beb802014a1", + "format": 1 + }, + { + "name": "tests/integration/targets/win_eventlog_entry/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7ef7d2adf23009f3e78563b83f26b0c36f1d35d130416f09cb854eed2a271e37", + "format": 1 + }, + { + "name": "tests/integration/targets/win_eventlog_entry/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1afcc2aeb0a4a4786e8345e0738aef8df6eaee36d40e45e0d0e54a500de582d4", + "format": 1 + }, + { + "name": "tests/integration/targets/win_feature_info", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_feature_info/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_feature_info/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fb7a2e394be672bb0b07a3a8861fc8424bf91aa3a8925d5fd241fdd3c92179b2", + "format": 1 + }, + { + "name": "tests/integration/targets/win_feature_info/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_feature_info/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b3e88291aff5fbf983787d922aee1a4475d24729d16e6ac71c3f3177523032f4", + "format": 1 + }, + { + "name": "tests/integration/targets/win_feature_info/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1afcc2aeb0a4a4786e8345e0738aef8df6eaee36d40e45e0d0e54a500de582d4", + "format": 1 + }, + { + "name": "tests/integration/targets/win_file_compression", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_file_compression/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_file_compression/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "607aa85feefd1482b6b6cf271de7f18f18a92e18991a5b97866462c0115c7ef6", + "format": 1 + }, + { + "name": "tests/integration/targets/win_file_compression/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_file_compression/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7df6cd7eb977d8bebdc11bf664ed83e01fd821f94cbeae22309fca5a38014700", + "format": 1 + }, + { + "name": "tests/integration/targets/win_file_compression/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_file_compression/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0a8d35cff0f6bf65c2e6b74d6f182b013d264f27a05cdcd98a14f0800fbfad7b", + "format": 1 + }, + { + "name": "tests/integration/targets/win_file_compression/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1afcc2aeb0a4a4786e8345e0738aef8df6eaee36d40e45e0d0e54a500de582d4", + "format": 1 + }, + { + "name": "tests/integration/targets/win_firewall", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_firewall/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_firewall/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4b8225e22256a73129bcfde8867ff681dba68a91f8b0a30c2c86c34455ccaa86", + "format": 1 + }, + { + "name": "tests/integration/targets/win_firewall/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e338e88e08da13dc870db5f771832f59890095bc71b1be3bb7c6de8f10aea93a", + "format": 1 + }, + { + "name": "tests/integration/targets/win_firewall/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7f5f4eb215f2a504baa753e868434b6942a07378e5d8a65ded04fd355964c8fa", + "format": 1 + }, + { + "name": "tests/integration/targets/win_firewall_rule", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_firewall_rule/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_firewall_rule/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6e0ac5beee1747da450cf22b493270b9bcb56577f876a070262923ba3bcea7eb", + "format": 1 + }, + { + "name": "tests/integration/targets/win_firewall_rule/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5663f103af71f218bb9a50a0d25b7dd6cc83f8761d3ef3c93c7f697f6b6bd1c0", + "format": 1 + }, + { + "name": "tests/integration/targets/win_format", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_format/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_format/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7df6cd7eb977d8bebdc11bf664ed83e01fd821f94cbeae22309fca5a38014700", + "format": 1 + }, + { + "name": "tests/integration/targets/win_format/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_format/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "caf4a8024c4bc4133974d5f64086f381e7acbae8e4a4b005bbd896e14751ff5d", + "format": 1 + }, + { + "name": "tests/integration/targets/win_format/tasks/pre_test.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7c0849818d5413031a20e8a0d22152d8bb8ee8428b87df32ae335ef1935c5e2b", + "format": 1 + }, + { + "name": "tests/integration/targets/win_format/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c9a828843ce7afdf58c3690b3a434bc53dbe38b82a94641068766552c4eef494", + "format": 1 + }, + { + "name": "tests/integration/targets/win_format/templates", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_format/templates/partition_creation_script.j2", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c06015e67cdb473a313de91d22f60d6e9aa1109d2b4ce79ce5b412b154d0d87d", + "format": 1 + }, + { + "name": "tests/integration/targets/win_format/templates/partition_deletion_script.j2", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f1319734e0deb3e19ae8c1f4c36557cf4a2c53189bdff2ac88fe315af8108877", + "format": 1 + }, + { + "name": "tests/integration/targets/win_format/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d7127ace6c1571fdf343a57b543cd35ed5b5181a330ab6e79ea892bf1ffff082", + "format": 1 + }, + { + "name": "tests/integration/targets/win_hosts", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_hosts/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_hosts/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "eeb6ca10a48e464bfceaea6ebe841222dc50f3608d7c29aad6852842f87a6e33", + "format": 1 + }, + { + "name": "tests/integration/targets/win_hosts/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_hosts/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7df6cd7eb977d8bebdc11bf664ed83e01fd821f94cbeae22309fca5a38014700", + "format": 1 + }, + { + "name": "tests/integration/targets/win_hosts/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_hosts/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5eb53945525d9f127627b1baa102effda2e4ced2a7d2b1e8a07e040187bea124", + "format": 1 + }, + { + "name": "tests/integration/targets/win_hosts/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5e2960f750230d1fbc9de1b9f8aba1603e60f14efeaafc71594935961d355f62", + "format": 1 + }, + { + "name": "tests/integration/targets/win_hosts/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5663f103af71f218bb9a50a0d25b7dd6cc83f8761d3ef3c93c7f697f6b6bd1c0", + "format": 1 + }, + { + "name": "tests/integration/targets/win_hotfix", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_hotfix/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_hotfix/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4979e45d7f2574ac4c11316a5be7b1fb397c3bccc677410dd3e347556fa31466", + "format": 1 + }, + { + "name": "tests/integration/targets/win_hotfix/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_hotfix/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a8ff179cf2ef68eef59ef1a86f09427e8182241afd0733238afcef1253892150", + "format": 1 + }, + { + "name": "tests/integration/targets/win_hotfix/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4a6bb263ac75f5afd0ae078af49c4dec7b0651b277c5ffa881d3afd8826f000a", + "format": 1 + }, + { + "name": "tests/integration/targets/win_hotfix/tasks/tests_2012R2.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "84bce8489177d9f9bf135d4da8514c82df7f18404ae628992912e82304372c3d", + "format": 1 + }, + { + "name": "tests/integration/targets/win_hotfix/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "32539e992e295dbc3d4b23f407668f18cb0251e8b542de412415028d1a192eae", + "format": 1 + }, + { + "name": "tests/integration/targets/win_http_proxy", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_http_proxy/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_http_proxy/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "98e4297e3f9a136b282cf1deda37441365563da116518c99a442b9b934bc766a", + "format": 1 + }, + { + "name": "tests/integration/targets/win_http_proxy/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d70a5476b6a6f06d2bf405e8d152ffd668a0177b2913ef7cb177a16344924493", + "format": 1 + }, + { + "name": "tests/integration/targets/win_http_proxy/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5663f103af71f218bb9a50a0d25b7dd6cc83f8761d3ef3c93c7f697f6b6bd1c0", + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_virtualdirectory", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_virtualdirectory/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_virtualdirectory/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ae00a524028be6bcbe590b533f7004343bec3022cc8125237cdf671c12aa8858", + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_virtualdirectory/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_virtualdirectory/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e31974ae519d6c9a7fead704312d1ff982a51e14c2392c42e615b8f15383c5cb", + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_virtualdirectory/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_virtualdirectory/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "107f4a45e094aef0082f65368f57fbf815cc98d72ee61739babcd8508c4c2cc6", + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_virtualdirectory/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ca79db3b84b4df1d47c913f3df8f9958a642c386770ea5cc66ad7c8410efcc31", + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_virtualdirectory/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e7125b8882592258dcd5bc1fb2eeade57228ca18412b216bb1be0d5c87970952", + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webapplication", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webapplication/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webapplication/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "272b5bf53ee34a45461174bb3cf8c37cd0a24516818c5c1eb56d1c229f42f7d1", + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webapplication/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webapplication/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e31974ae519d6c9a7fead704312d1ff982a51e14c2392c42e615b8f15383c5cb", + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webapplication/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webapplication/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5f82762ca07329ac5ccb78e93221b81b147e2d2d3272368f8c362f80270d68d3", + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webapplication/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "919f4719886a17e7b7410bc01a9ac0c176c58b22936df949cc5773a8e280e04e", + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webapplication/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ac575e5ffd4d79acff28e75071b58e0b8e33ab431e643f5722333a699082e3b9", + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webapppool", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webapppool/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webapppool/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cfcc8ac7cbd65bfeb6b4930f406197ea9b90338d3f8ffdfe11c06a17b84057cc", + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webapppool/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webapppool/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d6f77944f44f4eca550a244b828631dfac528471b7a0f5009185a8719c256b4a", + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webapppool/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2586cb5635c7526c23d1a241166543de3c2ab00f41b77059ef954b05fbf5543e", + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webapppool/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "758d23e7651820af7b1af35846b0a80daee9e3c321fd9e93a0f60b929035610f", + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webbinding", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webbinding/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webbinding/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2b2d15f249d25a18679ee27fc489c6b56565f920e4079fe6b8235f7225dd54ed", + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webbinding/library", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webbinding/library/test_get_webbindings.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "041e1ab1537328f70ec764f11c80b3980dfdd18ee37152f02f02363a0cd6589e", + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webbinding/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webbinding/tasks/failures.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "73645d5c8ca35d93d23147a8061a96650398e147a2697a6650d57ef091651caa", + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webbinding/tasks/http.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2988c585c51ec9c5e9d0f393d54a093d48aed3144d292ce07c4a1b195226d2c1", + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webbinding/tasks/https-ge6.2.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e469c72764fb79c6c1ff012b7e7b4b9c3cc1ceaa9f435a126e1aeed49d99f5cb", + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webbinding/tasks/https-lt6.2.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c86f12a6188b10e255e714705ea27e8c83184b0e809ea6d3879165d246f1c91d", + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webbinding/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4db13ff0e70971771180f7127141ff0bf57dd29bff89bc7d4b082dfbe3a2cb45", + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webbinding/tasks/setup.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fa40e6dcf7fd69ba52b4ec36a3cdbae495ed9bd897ce7c0179405a7df80fda77", + "format": 1 + }, + { + "name": "tests/integration/targets/win_iis_webbinding/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1aabf18b3e493a2bf65e8256494940c15795202c0132ef951482454ec995d2da", + "format": 1 + }, + { + "name": "tests/integration/targets/win_inet_proxy", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_inet_proxy/library", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_inet_proxy/library/win_inet_proxy_info.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "802f94faa392e3437e2ca6b3344fd646d28de42b3003788dcf23813acce5b279", + "format": 1 + }, + { + "name": "tests/integration/targets/win_inet_proxy/library/win_phonebook_entry.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "696e116bbc4ef77ea59e805c6a1679c88a714bb748071722e3f8b1454c5e0660", + "format": 1 + }, + { + "name": "tests/integration/targets/win_inet_proxy/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_inet_proxy/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8e30ae89b12e8659c4da6f7abcc763f84d17126567052e7be70f23b6c8911d1e", + "format": 1 + }, + { + "name": "tests/integration/targets/win_inet_proxy/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fea0178bf766201b02680fa4e871d96132188a7daaa45cf86d9305f5df58c205", + "format": 1 + }, + { + "name": "tests/integration/targets/win_inet_proxy/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1aabf18b3e493a2bf65e8256494940c15795202c0132ef951482454ec995d2da", + "format": 1 + }, + { + "name": "tests/integration/targets/win_initialize_disk", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_initialize_disk/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_initialize_disk/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "332884642a59dce2b746313aed56aa96b8af7b9fe9036fe1dcba9e10b56ffc2e", + "format": 1 + }, + { + "name": "tests/integration/targets/win_initialize_disk/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_initialize_disk/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a919bbc05e30662072f7cffef03f934527564d1952fab12370f466f53e0e7a5b", + "format": 1 + }, + { + "name": "tests/integration/targets/win_initialize_disk/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "210c0808c54da6d88b0a160ca1a75307a664d782b000f567f4fc0cfa58eb3acc", + "format": 1 + }, + { + "name": "tests/integration/targets/win_initialize_disk/templates", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_initialize_disk/templates/vhdx_creation_script.j2", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6a0813286d132fa402e244572e0a486cb557d41467df9d2f9e501975552a56ed", + "format": 1 + }, + { + "name": "tests/integration/targets/win_initialize_disk/templates/vhdx_deletion_script.j2", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f1319734e0deb3e19ae8c1f4c36557cf4a2c53189bdff2ac88fe315af8108877", + "format": 1 + }, + { + "name": "tests/integration/targets/win_initialize_disk/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d7127ace6c1571fdf343a57b543cd35ed5b5181a330ab6e79ea892bf1ffff082", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/.gitattributes", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8995c68e4bd285cc85c77999ce1b49c4211b0c8df796a2cfde5b978dbf69d3e3", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/01_new_line_at_bof.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bd517f3e26cfc2f9c60bcd43aabe6e0e6c533cf1bb4c57e1a90329c407347bb1", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/02_new_line_at_eof.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a6c5af83aadcef8cca9ebb45a9631404922d51f9601d7ae59856c33690ec84c6", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/03_new_line_after_1.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0ee03d302c77aaf45dbb99a0a6bf5dfbf1a6af721561d3623a940ec0a73cf4a5", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/04_new_line_before_5.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a8765a94c48a02423fa727a31a98c5a7788b084cac97cb9fe234309c4bd46043", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/05_new_line_at_REF.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2ef25b1fb89743a1058018f82aefe1d59df584dc1e2e531663362acc25b2743f", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/06_remove_middle_line.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a074640a30d13ed3100058b78433541d4a1e03d710db696005aa968c1dd9e597", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/07_remove_line_5.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "465ec27afcb58918c736e0cb43797dbc9d966bb16791a2101f2a213d4b9db4d6", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/08_no_expected_change.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "465ec27afcb58918c736e0cb43797dbc9d966bb16791a2101f2a213d4b9db4d6", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/09_new_file.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "dd96989d943734993ecd4dfc86a3b6809640f9690690ae5579044525316d03ef", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/10_no_eof_new_at_eof.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "880f0f704d165811d3e6975a87339cc7e52ad2b66ba869ceb0d9f1cdd45be661", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/11_multiline_at_eof.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ba45da2c94ac4b76f7e26898776eb5cea5f731ae32cab6139080c43450fe9674", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/12_empty_file_add_at_eof.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "81e3662f3607b7b5ca97adec3365c2d6de35ae66860ace8a7e5d11f77887600f", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/13_new_4_with_backref.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "aeee21bec2b14d16485b08811bff8b83a7922da957b64806a52cf3741af57bbf", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/14_quoting_code.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "81af518bdbedb356827f9dea5d8cd754e904dfa3d0652f8b0ca63812ede50ada", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/15_single_quote.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "53ffccc1fa9439f0f6811ea5c6f834158fbcb5715c1ef9914b03af3b50224a75", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/16_multiple_quotes.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f2e0461b7632f08507377c9c6503ed9138ce4b8c9b3aad7c7e1586beadefff59", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/17_new_file_win.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "dd96989d943734993ecd4dfc86a3b6809640f9690690ae5579044525316d03ef", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/18_sep_win.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f53d459a15aea59b560ba31908036eb35a96ac939630fec8a8ef2d5731b5ee75", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/19_new_file_unix.text", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "dd96989d943734993ecd4dfc86a3b6809640f9690690ae5579044525316d03ef", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/20_sep_unix.text", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f53d459a15aea59b560ba31908036eb35a96ac939630fec8a8ef2d5731b5ee75", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/21_utf8_no_bom.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "eea8307d7d286dc302d27aee4f220529230b8b685299cb8d525dfbf871e52fd3", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/22_utf8_no_bom_line_added.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "602793aa51daeea40a863c0fde3164046596e830f32752967d3657406f110d1d", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/23_utf8_bom.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9e288ba6d0ad07017683ae64d70c387093bf840df7b01c8fb8a89cf31f962338", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/24_utf8_bom_line_added.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3ebc31b1fdb215b69e44b1ace12b3c8f7158d2022cc2c1134e461015c69fb8b", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/25_utf16.txt16", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1ee6c6f916e917665457efbd5dce45e2c730c74a4ca53fd8798ee847771e117e", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/26_utf16_line_added.txt16", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e70a053e84d2f6360ac05617abb317fdb0852bf5b8d85f5091572a6d6fc5d944", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/27_utf32.txt32", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2ba322779abd7736807013f4bce30807fc35ca843efcad76d224509f0d784781", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/28_utf32_line_added.txt32", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "361787e374a946cd3c6a35145c31106bea78213b38791bb04c32221b1801c8f1", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/29_no_linebreak.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7acaabc4085318c0d69787f56879e28dde5dc83aa81e889e3e4a867334b4c204", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/30_linebreaks_checksum_bad.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ec611dbe789262dc7bd552debacf131585dc8a17af33ef7e6e3315b17d1074cf", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/31_relative_path.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bd517f3e26cfc2f9c60bcd43aabe6e0e6c533cf1bb4c57e1a90329c407347bb1", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/expectations/99_README.md", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e377b6b52a0a6c831991f01701b0f2942994edac023d62f1dd7e4259ceeb4e60", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/test.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "03682d50f56b19e914f97529261347ad0535cf7e76f27cc6638bd4986c50020c", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/test_linebreak.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/test_quoting.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/testempty.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/files/testnoeof.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "95316432dd51dbd60756051d85437e9e7326c70150157db4e6e33e3353d73521", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7df6cd7eb977d8bebdc11bf664ed83e01fd821f94cbeae22309fca5a38014700", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e50367af1609b35f1aa89b324bba1a4610ba58aeca028bd4b2f510401bc04808", + "format": 1 + }, + { + "name": "tests/integration/targets/win_lineinfile/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5663f103af71f218bb9a50a0d25b7dd6cc83f8761d3ef3c93c7f697f6b6bd1c0", + "format": 1 + }, + { + "name": "tests/integration/targets/win_listen_ports_facts", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_listen_ports_facts/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_listen_ports_facts/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c41535a9b46bc461ef0161868b93c73732e96b6063840eb32596a629ee8269a8", + "format": 1 + }, + { + "name": "tests/integration/targets/win_listen_ports_facts/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d7127ace6c1571fdf343a57b543cd35ed5b5181a330ab6e79ea892bf1ffff082", + "format": 1 + }, + { + "name": "tests/integration/targets/win_mapped_drive", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_mapped_drive/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_mapped_drive/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a1ced6ccaed7fd343f7de90016dcf0596cf5ee4d437c19f319fdbc4aa3663d99", + "format": 1 + }, + { + "name": "tests/integration/targets/win_mapped_drive/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_mapped_drive/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3a823897036408ef81fca38968b9c1689efea1239f8cd272b8844c69bf8b3c36", + "format": 1 + }, + { + "name": "tests/integration/targets/win_mapped_drive/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b2388433d5db57d22467ce1efa9814c1ae839b2d88713a495a713a1581793b51", + "format": 1 + }, + { + "name": "tests/integration/targets/win_mapped_drive/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d7127ace6c1571fdf343a57b543cd35ed5b5181a330ab6e79ea892bf1ffff082", + "format": 1 + }, + { + "name": "tests/integration/targets/win_msg", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_msg/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_msg/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "83682ea249e95a10e0e01af9045524221c4509057fc541597e2bb9b4e227fba3", + "format": 1 + }, + { + "name": "tests/integration/targets/win_msg/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5a8d43061af195016cc364033092a9baebedb740008f0efbb07893eb34a8e3e3", + "format": 1 + }, + { + "name": "tests/integration/targets/win_net_adapter_feature", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_net_adapter_feature/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_net_adapter_feature/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "807dbe52645ccba45e7de5a3d6d59e6d0d9ab64018d4489259707fbda7798a3b", + "format": 1 + }, + { + "name": "tests/integration/targets/win_net_adapter_feature/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_net_adapter_feature/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3afac57daf4c3b40c691886282af47222d4a8532aeab4e44df42c20bfccb7e47", + "format": 1 + }, + { + "name": "tests/integration/targets/win_net_adapter_feature/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e29a727e00b609afe2915c73d075a52bcc32023b9ad6bd7b29fecc31605ae862", + "format": 1 + }, + { + "name": "tests/integration/targets/win_net_adapter_feature/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1afcc2aeb0a4a4786e8345e0738aef8df6eaee36d40e45e0d0e54a500de582d4", + "format": 1 + }, + { + "name": "tests/integration/targets/win_netbios", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_netbios/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_netbios/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "807dbe52645ccba45e7de5a3d6d59e6d0d9ab64018d4489259707fbda7798a3b", + "format": 1 + }, + { + "name": "tests/integration/targets/win_netbios/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_netbios/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ca2a252005ce5900d0adf890a91ba8654060eeacdbc214f9ecfa7e3a4ccfa901", + "format": 1 + }, + { + "name": "tests/integration/targets/win_netbios/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "297cffbd19ef579fc38544e416a10cd9afcb910b85cdf1720b4b573d7a83897a", + "format": 1 + }, + { + "name": "tests/integration/targets/win_netbios/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1afcc2aeb0a4a4786e8345e0738aef8df6eaee36d40e45e0d0e54a500de582d4", + "format": 1 + }, + { + "name": "tests/integration/targets/win_nssm", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_nssm/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_nssm/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f6395d2bf901dac05dc0716af48b34728b609766a860136700771ef2c267064e", + "format": 1 + }, + { + "name": "tests/integration/targets/win_nssm/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_nssm/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "92f2b42912355ba7fd19805552d4e3b15a5f55ecc473890e37a6ef48c7886b33", + "format": 1 + }, + { + "name": "tests/integration/targets/win_nssm/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_nssm/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e17c9edf28b48f080da734eb910987bc7a12f06d6fd09262b18c41634d1ad850", + "format": 1 + }, + { + "name": "tests/integration/targets/win_nssm/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6ea9f0f86fd003478b4a1b284f6ed3f278912e5dfe5bfd4825ac8a8a74195f3e", + "format": 1 + }, + { + "name": "tests/integration/targets/win_nssm/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1aabf18b3e493a2bf65e8256494940c15795202c0132ef951482454ec995d2da", + "format": 1 + }, + { + "name": "tests/integration/targets/win_pagefile", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_pagefile/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_pagefile/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f5d8b2f2addd275197aabdee23beb0828f20ddfa085b7a635662d2002345d282", + "format": 1 + }, + { + "name": "tests/integration/targets/win_pagefile/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8f86e04a63b77ef852a53d69c53b7cab329c1eb42abe50261745bc2b6f38d1b9", + "format": 1 + }, + { + "name": "tests/integration/targets/win_partition", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_partition/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_partition/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "67fdfa165667eb5df293ad6b9ae9131a49b22cecfc97a88943ac19b787eeade6", + "format": 1 + }, + { + "name": "tests/integration/targets/win_partition/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_partition/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "92f2b42912355ba7fd19805552d4e3b15a5f55ecc473890e37a6ef48c7886b33", + "format": 1 + }, + { + "name": "tests/integration/targets/win_partition/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_partition/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b11173553bfb63c218e5f4100c538d6d2cceb11a0f09771d0da768d50efa880e", + "format": 1 + }, + { + "name": "tests/integration/targets/win_partition/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6f34446e8fbb09e5e8623ef1b0b1c5f0f10b8508b921367502054333bc1de2dd", + "format": 1 + }, + { + "name": "tests/integration/targets/win_partition/templates", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_partition/templates/vhdx_creation_script.j2", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0b44b32d4809cdcb2fed1216911aaa76864b98b7e0624b726090ec0634928299", + "format": 1 + }, + { + "name": "tests/integration/targets/win_partition/templates/vhdx_deletion_script.j2", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f1319734e0deb3e19ae8c1f4c36557cf4a2c53189bdff2ac88fe315af8108877", + "format": 1 + }, + { + "name": "tests/integration/targets/win_partition/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5663f103af71f218bb9a50a0d25b7dd6cc83f8761d3ef3c93c7f697f6b6bd1c0", + "format": 1 + }, + { + "name": "tests/integration/targets/win_pester", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_pester/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_pester/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6089883e66c66166da6e59fe6f2b572349021d3992689c8131ec5d3e9c6db769", + "format": 1 + }, + { + "name": "tests/integration/targets/win_pester/files", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_pester/files/fail.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e4e2c19642055d50c8c2d33e44c99e197e7725ab40fa23cdf742458ea0fcbb8a", + "format": 1 + }, + { + "name": "tests/integration/targets/win_pester/files/test01.tests.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9e13d32f62de6e8f6b843279d6bb7b39cd4cbe7fbaf348266978bd737162157f", + "format": 1 + }, + { + "name": "tests/integration/targets/win_pester/files/test02.tests.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "032d51cab6f6002c4ba0e668d8871438d9146bcfe70de3d4680875943d68a0a3", + "format": 1 + }, + { + "name": "tests/integration/targets/win_pester/files/test03.tests.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "58c3dc91a7eda4d66c2f10354929b6c37be5f8682e7cb5afad1d9dfbc69ef6d8", + "format": 1 + }, + { + "name": "tests/integration/targets/win_pester/files/test04.tests.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5eb91c42351feba07c5178d16c43edfb2118e3cfbfcb72ba973198e12476e186", + "format": 1 + }, + { + "name": "tests/integration/targets/win_pester/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_pester/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "553c47f72f9902669c5e05495a7267648ff7a6a71658f7e51867f08f3151f6a4", + "format": 1 + }, + { + "name": "tests/integration/targets/win_pester/tasks/test.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "285f1f83323fee51d3f707e0a189c1a49749dd741b4e1a787e87a60050abc203", + "format": 1 + }, + { + "name": "tests/integration/targets/win_pester/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1aabf18b3e493a2bf65e8256494940c15795202c0132ef951482454ec995d2da", + "format": 1 + }, + { + "name": "tests/integration/targets/win_power_plan", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_power_plan/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_power_plan/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6816399b752ee180f2cf53225197902b23f8f78ead234495a4efd80a42f2dc7a", + "format": 1 + }, + { + "name": "tests/integration/targets/win_power_plan/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1afcc2aeb0a4a4786e8345e0738aef8df6eaee36d40e45e0d0e54a500de582d4", + "format": 1 + }, + { + "name": "tests/integration/targets/win_product_facts", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_product_facts/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_product_facts/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c3eb973e1fa8827ef61336287827e23e795458feeb099d00cce71ee346dc3a9e", + "format": 1 + }, + { + "name": "tests/integration/targets/win_product_facts/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5663f103af71f218bb9a50a0d25b7dd6cc83f8761d3ef3c93c7f697f6b6bd1c0", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psexec", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psexec/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psexec/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7df6cd7eb977d8bebdc11bf664ed83e01fd821f94cbeae22309fca5a38014700", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psexec/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psexec/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ad6501d6b2150da1bede8fe0b68013d61230aff6019eee46dc0a3cbd48dad678", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psexec/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d7127ace6c1571fdf343a57b543cd35ed5b5181a330ab6e79ea892bf1ffff082", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule/files", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule/files/module", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule/files/module/license.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3e61d673db4193e2e813b9345aa52ffc22b57cad28f9b82914aa5494aba776d1", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule/files/module/template.nuspec", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fea6f0f6e013f70d6f54bc151d99d38c90beba688ee50c36af7cdd02d47e4085", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule/files/module/template.psd1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3c123b79dc7d6c177aad7aec2340f3fd67c42bbbb58f056c792f416bc165160", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule/files/module/template.psm1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c3af1c293b061aeae55a75922f682b72fe073d23a0214a92ecdf0330ae236c74", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule/files/openssl.conf", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9715d89a84b15cdb10a5b81bccc3aa1b778436dd88688cfbdf13cde2f5cef6ad", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule/files/setup_certs.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4db82cd49211a1b4463ec62639d894099c781468198ea22d1448edc8f56d2a01", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule/files/setup_modules.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "71ab33b143470a2ca97ee569f906401cfcef2efdebd2758ef37c26df35c41624", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule/handlers", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule/handlers/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8c296762c130115006225789058786c935a3bf6d64577f5ccd5465377d2c4d89", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4fbb079185ffb8e152e7f51eb8e04da6e07956253b49baf0f61daf7b3cb0ef77", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1b2adee5ca3e7db150c1ad9adcbe1b485ac72c2d8fc9b614949a0414950a6784", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule/tasks/setup.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "91ab3cbcd9536663ad8f68671add4d794152923e3eb76c04d3aea439aa9e60c3", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5663f103af71f218bb9a50a0d25b7dd6cc83f8761d3ef3c93c7f697f6b6bd1c0", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule_info", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule_info/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule_info/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b04d0cf52c7f0bab54523594fc5a817b7ffcde294254ba45113071b36a34eb4c", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule_info/files", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule_info/files/ansiblevault.0.3.0.nupkg", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "38aa18424732d1da7bef9dd43fe92996fdc5f57472001f759f9c8c30d3e7d055", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule_info/files/pinvokehelper.0.1.0.nupkg", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a447fcfe73aa9f18fc3653c61f8bc46d5a982a0f2e57d51c782f9fa9da7ddcbf", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule_info/files/pscsharpinvoker.0.1.0.nupkg", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "21f4157eca7aa445ef762660ffca1060b0885b7c8b81080339621cf8ac027c2e", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule_info/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule_info/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "845e0a2e5969bb32652b9101d42fc5525d60a1b1c0d8e7002efda88ae9b6cc8f", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule_info/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule_info/tasks/common.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a8327b3c3a8a3fb70ce72061d167b6057e0fbbd3aab4d661d60143ae370f62c3", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule_info/tasks/contains_all_fields.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2db1f43bd61c0287f7b8b033e8b209744168fd9140965a6f2e7614b58a66d4dd", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule_info/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "56be57ba96a2f2fecf87294de63caca292cd88d20b3e2a4cf08995456130f74f", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule_info/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2f2cdaf24058c535eacd2f313b6ced70550ad72a1396ed6bc9a0e11a1805d98e", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psmodule_info/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1aabf18b3e493a2bf65e8256494940c15795202c0132ef951482454ec995d2da", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a54fc717b702d668a1bfb5c1ba0ef59950422060c38c9c5a233c20ae6e8ab4af", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a41f3880442c3cda4be32c8567b990bacca9f46ad1b28be0cd0c58d5cdee99a2", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository/tasks/get_repo_info.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ddcf35e22dcc257a86bc35911633fa11dc3fdd56f1fb903a8b56217e24c09f4b", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "df30e434c59f1358e65d9631fb43860e01d1c4c082aa2e73e8891060fd7736de", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "008ae753f3b80cc69c50bacfa3569afd96beb6d563dcd3702d6c67aaa588eb88", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository/tasks/update_and_force.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8302a6fae785950091e5be722233b63b6e8e3ca6e630e6feaba7e55a57ad0485", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "322fece9c856917b28535870b072819d73561d65c8cb6c482f5d7946b18f1c26", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_copy", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_copy/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_copy/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4bacd95c456df7d6310330f7443e5db34189c0a309673128cd10f0e838d1c699", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_copy/files", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_copy/files/SampleRepositories.xml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7502f482472c78cf7467f7fe3d17a5ce71881cfafde356a13f8c846428e6f081", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_copy/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_copy/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "845e0a2e5969bb32652b9101d42fc5525d60a1b1c0d8e7002efda88ae9b6cc8f", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_copy/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_copy/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a95fad684caaa84181f629eb596ee954a184438518adb46aff01fad2786b5a53", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_copy/tasks/remove_test_profiles.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "35ce43a4e1eac04e52069b8d7e19abf1a67e8a95bbc0eed268f79cf764f27aad", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_copy/tasks/reset.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5c3d76dd6aca1e527ca70e680e2b18bfb3abc40021adc4afead01f286e0e0ca0", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_copy/tasks/test_by_user.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "aa5a2aedebd5a3bd31a8f39d0ff56b0ed5c3feef581928a6dfd27cf769a2e863", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_copy/tasks/test_exclude_profile.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "74150d8855aa3d7e1ed75614cf905b3af1945ecdacd659615123e0319d95eff1", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_copy/tasks/test_exclude_repo.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d38535b2b75fb2f4e7e6d5ac1512c0980f2d7fa0c80f8671fe6486ff87a5f8b7", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_copy/tasks/test_include_profile.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c9a6e2f17d54e64d26a96099530674e477c84824ec64a848d6da5a59261500ee", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_copy/tasks/test_include_repo.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "eb5d95da312820283745eb81aedb68d701e963a898b8b683801819a5c558ca03", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_copy/tasks/test_system_users.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "83a3c512a08614df2ffdc0358b6ed89df9be342213afff653d087764f3ec2d32", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_copy/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1afcc2aeb0a4a4786e8345e0738aef8df6eaee36d40e45e0d0e54a500de582d4", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_info", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_info/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_info/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d7b9544dc6bbdd48bf1624137e662eaa3c38fc4459f53144ec016368d052972d", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_info/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_info/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0c8b774943ae1182244f88b804c2a9609b0756ef7833fc2d6beb4ecff2b965b4", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_info/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_info/tasks/contains_all_fields.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9e4b5f00b10183982fb68c32739123b51a67ecbc61031975c745b74b7b2933fc", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_info/tasks/empty.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a8aaf83fa8b3e0e33152574498e5f61e354f50d472376a13d06743926696d3f5", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_info/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3db97d55e800176f5029fea2a6ea1e40117d45340654a6d8db9a7905abc973fc", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_info/tasks/multiple.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0754a2fce4cf30e924e4e17111aa033aebea8d786e80e5b7d5983dcfc45d83cf", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_info/tasks/single.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f606f69f870843d0144a7558ca0b14d087c96d6204b05c46295788fa32168ecc", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psrepository_info/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "eea80c0dec844bd560d4c6bfb07e5f82872a1843d889fdf38f17b826f6bde38a", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c9054dcec38ca371dea8b0df1247625bf1859495173cc88b6648d7f435c8b279", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript/handlers", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript/handlers/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0a3029fb8aa7ae125500ea27af8c2d0d6a6647057ab5a9ec79ae5f871419ad0a", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "845e0a2e5969bb32652b9101d42fc5525d60a1b1c0d8e7002efda88ae9b6cc8f", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6931841d3173abfff02e1e3fab721605aa9ddd829e067664d408de21ca6055f3", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript/tasks/script_info.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "69af2fef7dbf26c17e3db8af0355381f285094c82b3625d5aa18394a608e383d", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript/tasks/setup_repos.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d9e9265fd79c53f6362598dd01b2e7025384d8c68a55dab18fdd8a80ef887f68", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "74947feb0266d3b89ae275204e2121c1e5361d281a60e3429bc34b61ac944610", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d7127ace6c1571fdf343a57b543cd35ed5b5181a330ab6e79ea892bf1ffff082", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript_info", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript_info/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript_info/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e923db01c06baf83ac9565c8eabe92e34b8ac7f11cae7a3a04dfa6610b7a868b", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript_info/files", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript_info/files/install-git.1.0.5.nupkg", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8b65c33bb6e9ab4a0d3b7cb44ed8da3d6f66eec40acf07cfa734a55101aafbda", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript_info/files/install-wmf3hotfix.1.0.0.nupkg", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "eb9c5b85ac2d2415ce7beabf1cdebbff197039b14a4fcdfcd541fe7e6e5e9c57", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript_info/files/test-rpc.1.0.0.nupkg", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "01afede115916bd5fb098620c5fd370fc3524785609266f12d6b27b20e99d68b", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript_info/files/upgrade-powershell.1.0.0.nupkg", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1747e2ac401698e46b08f6ad61174a0a1fa197b2dd8baa790e63a86f7382dd68", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript_info/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript_info/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "845e0a2e5969bb32652b9101d42fc5525d60a1b1c0d8e7002efda88ae9b6cc8f", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript_info/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript_info/tasks/common.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3ec6651c1948fbd207fcfb6898224d40e6c98e5153955fac07a09a73397c2177", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript_info/tasks/contains_all_fields.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2db1f43bd61c0287f7b8b033e8b209744168fd9140965a6f2e7614b58a66d4dd", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript_info/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2142d81208ca30e40b4deb28b86152020f7d2b1bb2e313c2cb8322a57ae28fa8", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript_info/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "29fd6f8c8f850a7101225ce02cbcd6f62acc349b9099baec4f15ac5a6b2113ce", + "format": 1 + }, + { + "name": "tests/integration/targets/win_psscript_info/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "758d23e7651820af7b1af35846b0a80daee9e3c321fd9e93a0f60b929035610f", + "format": 1 + }, + { + "name": "tests/integration/targets/win_pssession_configuration", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_pssession_configuration/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_pssession_configuration/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0032481091b74f5fcf14321eeef6cb709b8860e22a3d2f06d593a63ab1a39709", + "format": 1 + }, + { + "name": "tests/integration/targets/win_pssession_configuration/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_pssession_configuration/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0ec0577229ca35e2aa70ac93418a6243bf847b2812dbe4cc4fe3d9cf9deec62a", + "format": 1 + }, + { + "name": "tests/integration/targets/win_pssession_configuration/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_pssession_configuration/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d6d4ce593ab5a0fa8929b41405061fa6c11bebeaf18733c00cafdb0cf24343b3", + "format": 1 + }, + { + "name": "tests/integration/targets/win_pssession_configuration/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "18e1afe5dd98b018abd6c83c02e97c1c2a16f1bfaf7c8eb3ffcf170bf92aebcb", + "format": 1 + }, + { + "name": "tests/integration/targets/win_pssession_configuration/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "758d23e7651820af7b1af35846b0a80daee9e3c321fd9e93a0f60b929035610f", + "format": 1 + }, + { + "name": "tests/integration/targets/win_rabbitmq_plugin", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_rabbitmq_plugin/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_rabbitmq_plugin/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a39cb045a1ef3e2cd55696690b9759b6528f60b6df35a14516125ed36562bd0f", + "format": 1 + }, + { + "name": "tests/integration/targets/win_rabbitmq_plugin/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4df29eb09b67546f68f0329153e4cfca8b8d19548d147bd0ddae7bb14a754dbb", + "format": 1 + }, + { + "name": "tests/integration/targets/win_rabbitmq_plugin/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8025d5b6d7bc29f42636afb5c8bbab561ef4a099aa0c508c2ea14bac9dd399bd", + "format": 1 + }, + { + "name": "tests/integration/targets/win_rds", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_rds/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_rds/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "21ef25a8b17ce4238d4053d70eae0f19662b054b0e04f0dd7ab8778583acae44", + "format": 1 + }, + { + "name": "tests/integration/targets/win_rds/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_rds/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "92f2b42912355ba7fd19805552d4e3b15a5f55ecc473890e37a6ef48c7886b33", + "format": 1 + }, + { + "name": "tests/integration/targets/win_rds/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_rds/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5776a1b7c4fba502b1e103ba1ad2059f04a4d7d7f9f7fb1e8383fd47f4875db2", + "format": 1 + }, + { + "name": "tests/integration/targets/win_rds/tasks/win_rds_cap.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d635b96b2de7ed2e103f05cd7d03bca764360197a1f5ed4f0f2f06ee5e980bd0", + "format": 1 + }, + { + "name": "tests/integration/targets/win_rds/tasks/win_rds_cap_tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "efed7e87c732b93ac76a9b78bed0b3fc13a56ece8ef863a2864a378efd7f42d9", + "format": 1 + }, + { + "name": "tests/integration/targets/win_rds/tasks/win_rds_rap.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d10658be2de14219858b7b32b6d212bcff1d752a78b9709724ad48faa664f524", + "format": 1 + }, + { + "name": "tests/integration/targets/win_rds/tasks/win_rds_rap_tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "aedc05bfdebc1c00f82b2591e6250990c4655801a2ed4c7c3310788967eecf67", + "format": 1 + }, + { + "name": "tests/integration/targets/win_rds/tasks/win_rds_settings.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "81196c3ef0208bab7e22e1a87dccf523297a139953b63f01b51a04827668a5e2", + "format": 1 + }, + { + "name": "tests/integration/targets/win_rds/tasks/win_rds_settings_tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "833217289bb01d4f1f0bc3c85af645c0fe2d47cc9a55ebeeb02693dcc8178798", + "format": 1 + }, + { + "name": "tests/integration/targets/win_rds/templates", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_rds/templates/rds_base_cfg.xml.j2", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e2ef6e1b50a0dfe0bd39e03d3e7a0562e7641a37332ec0f7f521d838e2fafb0b", + "format": 1 + }, + { + "name": "tests/integration/targets/win_rds/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b0c52f35debc166c9da2709a0e40f18a088f1dad16449978bfb19e230f170601", + "format": 1 + }, + { + "name": "tests/integration/targets/win_region", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_region/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_region/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "614e21af3f60a208f7f3dc87513f41676369d3500863f453b5582f94d9052a0c", + "format": 1 + }, + { + "name": "tests/integration/targets/win_region/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "758d23e7651820af7b1af35846b0a80daee9e3c321fd9e93a0f60b929035610f", + "format": 1 + }, + { + "name": "tests/integration/targets/win_regmerge", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_regmerge/files", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_regmerge/files/settings1.reg", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "936d9385cfdd32b6dc1ec2dbcc601dadb9320d5051ecb0d762452c7bb7cea9ab", + "format": 1 + }, + { + "name": "tests/integration/targets/win_regmerge/files/settings2.reg", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5623892c77d8eed8a0880a9f7df8069ff70fa975a697f6a9a764001ef5fed06d", + "format": 1 + }, + { + "name": "tests/integration/targets/win_regmerge/files/settings3.reg", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "76dbceb93973dc172e1ae17d9f62c48a4fbd19ae3ff080209b917a81f5c2449d", + "format": 1 + }, + { + "name": "tests/integration/targets/win_regmerge/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_regmerge/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7df6cd7eb977d8bebdc11bf664ed83e01fd821f94cbeae22309fca5a38014700", + "format": 1 + }, + { + "name": "tests/integration/targets/win_regmerge/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_regmerge/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "334193f120c8b5dff8a7ce25f0b81f5b7eb2b47aea9ef93ea44fe77ad418cc96", + "format": 1 + }, + { + "name": "tests/integration/targets/win_regmerge/templates", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_regmerge/templates/win_line_ending.j2", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6d8f28b50f247de57341be86bb6944cfd9b019b6fb5f19c665fae3f7bc4115cf", + "format": 1 + }, + { + "name": "tests/integration/targets/win_regmerge/vars", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_regmerge/vars/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9cb74160f3ba97cff957cea38dbe436aec4b1fdf3d7b47ee67df6bd2b5992fb3", + "format": 1 + }, + { + "name": "tests/integration/targets/win_regmerge/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1aabf18b3e493a2bf65e8256494940c15795202c0132ef951482454ec995d2da", + "format": 1 + }, + { + "name": "tests/integration/targets/win_route", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_route/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_route/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a9c3dc42db269b752fa572e06c7fb75b1bbef33328c20ac53458ab1636e902e7", + "format": 1 + }, + { + "name": "tests/integration/targets/win_route/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_route/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "131541f93918493fa5b08f5c3530f3f061a9ba09527b6d30ba7a2aa5fdb983e9", + "format": 1 + }, + { + "name": "tests/integration/targets/win_route/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "08818fa34a25bf51aa8f1b420c2e0240ab92ae6f77f16fbe911cc2c311b29f58", + "format": 1 + }, + { + "name": "tests/integration/targets/win_route/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d7127ace6c1571fdf343a57b543cd35ed5b5181a330ab6e79ea892bf1ffff082", + "format": 1 + }, + { + "name": "tests/integration/targets/win_say", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_say/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_say/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8205635633623a7583e3e0da7605a93092490e2a4236a90958403f9a3b18a9e2", + "format": 1 + }, + { + "name": "tests/integration/targets/win_say/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5663f103af71f218bb9a50a0d25b7dd6cc83f8761d3ef3c93c7f697f6b6bd1c0", + "format": 1 + }, + { + "name": "tests/integration/targets/win_scheduled_task", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_scheduled_task/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_scheduled_task/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9a9c5fb5d32be6ae4794c82dea757bebec24d62bb379c988cb5300a5629a941d", + "format": 1 + }, + { + "name": "tests/integration/targets/win_scheduled_task/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_scheduled_task/tasks/clean.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3df7268a04005a6e548150a9a9957c83f3231ed0ed3136f1cbc675cecf4fb734", + "format": 1 + }, + { + "name": "tests/integration/targets/win_scheduled_task/tasks/failures.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9c8b6c12249764c44c9c49e85f8865a04fa1cb9a1d72ecfa88b28dd735a2df11", + "format": 1 + }, + { + "name": "tests/integration/targets/win_scheduled_task/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9179593a38b37f72a9527b6cb2ee53ee11b83b93118bcd7986a856e1fc9bac18", + "format": 1 + }, + { + "name": "tests/integration/targets/win_scheduled_task/tasks/principals.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "14e54a1e11a955baa032a5f44de025606d147acdc0eca3751127789eba20dd40", + "format": 1 + }, + { + "name": "tests/integration/targets/win_scheduled_task/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d71893bc44825fd38361773fa07420181af02fa650b4afbc01a858d49e3f1043", + "format": 1 + }, + { + "name": "tests/integration/targets/win_scheduled_task/tasks/triggers.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "276ab72db53df66741541bbf8307dc0d93d2223b6d05ca703606736e3b377618", + "format": 1 + }, + { + "name": "tests/integration/targets/win_scheduled_task/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d7127ace6c1571fdf343a57b543cd35ed5b5181a330ab6e79ea892bf1ffff082", + "format": 1 + }, + { + "name": "tests/integration/targets/win_scheduled_task_stat", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_scheduled_task_stat/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_scheduled_task_stat/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "14d0cfe474bfdab3a4f9ba0b09aa652ae31268d1541d59731987c633558a7a23", + "format": 1 + }, + { + "name": "tests/integration/targets/win_scheduled_task_stat/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_scheduled_task_stat/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fb58542712388a10a9235968ba4c188c492348e30f99fb4991e4c6e13bb5aac9", + "format": 1 + }, + { + "name": "tests/integration/targets/win_scheduled_task_stat/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "06cc33be8fdf0d0190f68b4d792ee586ad55bc6172f98753801b0e409d81bb72", + "format": 1 + }, + { + "name": "tests/integration/targets/win_scheduled_task_stat/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1aabf18b3e493a2bf65e8256494940c15795202c0132ef951482454ec995d2da", + "format": 1 + }, + { + "name": "tests/integration/targets/win_scoop", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_scoop/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_scoop/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e149a4b7cc799294e87b4c9290fa215b7277d9bdb66ac1f8a9a4afaecd99b901", + "format": 1 + }, + { + "name": "tests/integration/targets/win_scoop/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_scoop/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0a99edc0baeac96e03e79952458ac590f7121127918d4ea70e5409590361ce7c", + "format": 1 + }, + { + "name": "tests/integration/targets/win_scoop/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "77726569ebf9d377f7a3bf3db2cb8e326429178a63a7394d1750201f05306f7e", + "format": 1 + }, + { + "name": "tests/integration/targets/win_scoop/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7ca8e11325c78f2d61739d8993cb9846c41a74083f1a52399e6bafd8ffcbe2c7", + "format": 1 + }, + { + "name": "tests/integration/targets/win_scoop_bucket", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_scoop_bucket/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_scoop_bucket/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e31a5bc94b972e92f0454a3fd1153b882b5e63d8a25da5bae75613ed9788496b", + "format": 1 + }, + { + "name": "tests/integration/targets/win_scoop_bucket/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_scoop_bucket/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "437ce69f730e0a56e5a9a7f6aeed6631634b265cd09f9cb3b73fa3a523a22f0f", + "format": 1 + }, + { + "name": "tests/integration/targets/win_scoop_bucket/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3dd57ba2ee17c98ab3d3cc56a6cc4d71a89ea06e7d91bc95fdd17adbdfb46f34", + "format": 1 + }, + { + "name": "tests/integration/targets/win_scoop_bucket/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "dfe04062f14c840d5f0ba976715b69d7f347084312eee07a233081f28bd78bb6", + "format": 1 + }, + { + "name": "tests/integration/targets/win_security_policy", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_security_policy/library", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_security_policy/library/test_win_security_policy.ps1", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e7be0283467be29dca02783fc7b1f55a5eaa4207eaea2ce403ec42151f3cd0b0", + "format": 1 + }, + { + "name": "tests/integration/targets/win_security_policy/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_security_policy/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b2e2a3b25133c28afdfdc12e6f1fdb1439dbabbde6db6e96b6828ac3553c79d5", + "format": 1 + }, + { + "name": "tests/integration/targets/win_security_policy/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "021220a33044bb45455d05fcca85e803360bc82d9acd9da66910fcbfea5fa410", + "format": 1 + }, + { + "name": "tests/integration/targets/win_security_policy/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5663f103af71f218bb9a50a0d25b7dd6cc83f8761d3ef3c93c7f697f6b6bd1c0", + "format": 1 + }, + { + "name": "tests/integration/targets/win_shortcut", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_shortcut/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_shortcut/tasks/clean.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "722d7c2ed6f779938c74b65928976387779964d4e4d7e4e7318ac035b6086a38", + "format": 1 + }, + { + "name": "tests/integration/targets/win_shortcut/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2f4b6af62f620a93d63ab04f186f10267ab6bdadb2506e72e04f027feb688ff3", + "format": 1 + }, + { + "name": "tests/integration/targets/win_shortcut/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8d309198d6ee0e546f6ba9489f8db44d15e3118a84fa23c8f19a09d1388f09c6", + "format": 1 + }, + { + "name": "tests/integration/targets/win_shortcut/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5663f103af71f218bb9a50a0d25b7dd6cc83f8761d3ef3c93c7f697f6b6bd1c0", + "format": 1 + }, + { + "name": "tests/integration/targets/win_snmp", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_snmp/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_snmp/tasks/cleanup.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a972b042376cdb918eaf0ba2ceab750e28e9245804bf3755e57596b5eb53b744", + "format": 1 + }, + { + "name": "tests/integration/targets/win_snmp/tasks/cleanup_using_module.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8a9ec3718442f1a628e89e96fba166f35544bf4f9e1ffe3e5250ef313bf2f450", + "format": 1 + }, + { + "name": "tests/integration/targets/win_snmp/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6693c7619fa952cafb43619cae244fdacb75b373641a0eea26857e26e0571dfe", + "format": 1 + }, + { + "name": "tests/integration/targets/win_snmp/tasks/output_only.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7c95c62d219a51e18244f63cb266c5c5a9739d57973cbbcf1c48b803f39a2002", + "format": 1 + }, + { + "name": "tests/integration/targets/win_snmp/tasks/snmp_community.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bee893eef71b6f2e0e24c21030b4896eade761a1984b17085f7131c441a8cdc3", + "format": 1 + }, + { + "name": "tests/integration/targets/win_snmp/tasks/snmp_managers.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ebf2846caecdfcd9a130ecf9c4a155a914c4a112bb7cd2c1b8792675f25ef1a9", + "format": 1 + }, + { + "name": "tests/integration/targets/win_snmp/vars", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_snmp/vars/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2fb738751e8143846bd31eec90ca19332ff6e2025f5d4551c6366d8862187ac1", + "format": 1 + }, + { + "name": "tests/integration/targets/win_snmp/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "758d23e7651820af7b1af35846b0a80daee9e3c321fd9e93a0f60b929035610f", + "format": 1 + }, + { + "name": "tests/integration/targets/win_timezone", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_timezone/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_timezone/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3317aaafcfdcdb1fa6e12f8c5de3a19ae2560d1b5dcbe17bb7274a7e5bd17701", + "format": 1 + }, + { + "name": "tests/integration/targets/win_timezone/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "305a1d99d626693e6cba079d8f8c93c950fca2cc7a380af130361b1ef8d13dfa", + "format": 1 + }, + { + "name": "tests/integration/targets/win_timezone/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1afcc2aeb0a4a4786e8345e0738aef8df6eaee36d40e45e0d0e54a500de582d4", + "format": 1 + }, + { + "name": "tests/integration/targets/win_toast", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_toast/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_toast/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "903680bc48f5073a20dffc176b9d0462267a238ce492c7981a8c80324dff6b62", + "format": 1 + }, + { + "name": "tests/integration/targets/win_toast/tasks/setup.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b447fdae35b1ea6039bd608db897bf07984006afe82e728989a22823b604e932", + "format": 1 + }, + { + "name": "tests/integration/targets/win_toast/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "208d60137e6aed14b636388d1a9f3ab0b568b9d6e1a0382489e2c1e78e63883e", + "format": 1 + }, + { + "name": "tests/integration/targets/win_toast/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cce960c8aadf19e754f600b163c5acdbb2cb67a6f7c14fac02ca6cdc469b4d67", + "format": 1 + }, + { + "name": "tests/integration/targets/win_unzip", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_unzip/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_unzip/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c490cd82ce3871ef5de0f5297743167ff5a67996632247c982e907221e3e3af0", + "format": 1 + }, + { + "name": "tests/integration/targets/win_unzip/files", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_unzip/files/create_crafty_zip_files.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a0da2d0011f1e5f08d64138fa79f624eff3231410b7f7e66b7df7a02dfd559ea", + "format": 1 + }, + { + "name": "tests/integration/targets/win_unzip/files/create_zip.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "54be0cb14739661c8eb4fad13d69fc34e19b4052be1d6a6d32a152b2ef1f31a7", + "format": 1 + }, + { + "name": "tests/integration/targets/win_unzip/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_unzip/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7df6cd7eb977d8bebdc11bf664ed83e01fd821f94cbeae22309fca5a38014700", + "format": 1 + }, + { + "name": "tests/integration/targets/win_unzip/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_unzip/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c1e092d194520e6df39f77bb847575f0102981edc05e969e987bd584b8dee0c5", + "format": 1 + }, + { + "name": "tests/integration/targets/win_unzip/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d7127ace6c1571fdf343a57b543cd35ed5b5181a330ab6e79ea892bf1ffff082", + "format": 1 + }, + { + "name": "tests/integration/targets/win_user_profile", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_user_profile/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_user_profile/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "738f6a2bd897edbc4e17d866a9718016e4922d599d9b199016dbddc08d6fe5c0", + "format": 1 + }, + { + "name": "tests/integration/targets/win_user_profile/tasks/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "833f3538f6b139c8cf86ea3296dba63d36c7d2b1a99144b52b6a14fa8bda8ea2", + "format": 1 + }, + { + "name": "tests/integration/targets/win_user_profile/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1afcc2aeb0a4a4786e8345e0738aef8df6eaee36d40e45e0d0e54a500de582d4", + "format": 1 + }, + { + "name": "tests/integration/targets/win_wait_for_process", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_wait_for_process/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_wait_for_process/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0750428c8a47fceb84c2743033d1724977d792f2fb3f112597f066139723a519", + "format": 1 + }, + { + "name": "tests/integration/targets/win_wait_for_process/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1afcc2aeb0a4a4786e8345e0738aef8df6eaee36d40e45e0d0e54a500de582d4", + "format": 1 + }, + { + "name": "tests/integration/targets/win_wakeonlan", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_wakeonlan/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_wakeonlan/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f8653f2b37f128be558da75080e0d4843c3cc099d9db6b42cabaf2b9e196430d", + "format": 1 + }, + { + "name": "tests/integration/targets/win_wakeonlan/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "758d23e7651820af7b1af35846b0a80daee9e3c321fd9e93a0f60b929035610f", + "format": 1 + }, + { + "name": "tests/integration/targets/win_xml", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_xml/files", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_xml/files/books.xml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "38df75a04da46b0867b55798f343c2e38bf9e0e6a51ef4297c031c18f5d9f26d", + "format": 1 + }, + { + "name": "tests/integration/targets/win_xml/files/config.xml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ac48bf5905e9ea640320878d224d36b68d2f9ff94a764129ef41f5697282b20a", + "format": 1 + }, + { + "name": "tests/integration/targets/win_xml/files/log4j.xml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ed8d276235d95fdd2d95d8252ce08fa21bfac45c126db516efdf835865894011", + "format": 1 + }, + { + "name": "tests/integration/targets/win_xml/files/plane.zip", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "27e5385fb70940a1dc17a36057ab8c90d4135c5a4223942eda3436d49c14a2ec", + "format": 1 + }, + { + "name": "tests/integration/targets/win_xml/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_xml/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7df6cd7eb977d8bebdc11bf664ed83e01fd821f94cbeae22309fca5a38014700", + "format": 1 + }, + { + "name": "tests/integration/targets/win_xml/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_xml/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a8d9362363f888a3da186e403e0c79ecde6c93eeb97e4260c84d54647377f8ab", + "format": 1 + }, + { + "name": "tests/integration/targets/win_xml/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "758d23e7651820af7b1af35846b0a80daee9e3c321fd9e93a0f60b929035610f", + "format": 1 + }, + { + "name": "tests/integration/targets/win_zip", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_zip/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_zip/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6023a332faadff97676aa9f6e9790f0a18b1ef02aa104469e05f8862ad90b300", + "format": 1 + }, + { + "name": "tests/integration/targets/win_zip/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_zip/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7df6cd7eb977d8bebdc11bf664ed83e01fd821f94cbeae22309fca5a38014700", + "format": 1 + }, + { + "name": "tests/integration/targets/win_zip/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/win_zip/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f8cc1b1f9c994a6af2679433d12ceaa9847fa97d65bba48d782753346c313628", + "format": 1 + }, + { + "name": "tests/integration/targets/win_zip/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "26e78414fbe1fd8a0602e3ff788789a0dfa5932636bf553948d524ee05ecc23e", + "format": 1 + }, + { + "name": "tests/sanity", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.12.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "adbb2b958c5689ab8a5376528853a261f23292a7386a16f2ee445894b513ccfc", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.13.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "adbb2b958c5689ab8a5376528853a261f23292a7386a16f2ee445894b513ccfc", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.14.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "adbb2b958c5689ab8a5376528853a261f23292a7386a16f2ee445894b513ccfc", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.15.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "adbb2b958c5689ab8a5376528853a261f23292a7386a16f2ee445894b513ccfc", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.16.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "adbb2b958c5689ab8a5376528853a261f23292a7386a16f2ee445894b513ccfc", + "format": 1 + }, + { + "name": "tests/unit", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/compat", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/compat/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/unit/compat/mock.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a4c95e1616f09fd8cecc228b798dc4a15936d96764e3d9ccdfd7a0d65bec38e4", + "format": 1 + }, + { + "name": "tests/unit/mock", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/mock/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/unit/mock/loader.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3b51ec0d45347a3568300e50f688998007e346f052bd2e961c2ac6d13f7cee4d", + "format": 1 + }, + { + "name": "tests/unit/mock/path.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "94808cc4dc172c6b671c9b01db06c497c2af358ab989d16ab0638701e88f1921", + "format": 1 + }, + { + "name": "tests/unit/mock/procenv.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f924e8a25cfa0531e00bc35df6e3b9790469302296f662c225374b12797bba7b", + "format": 1 + }, + { + "name": "tests/unit/mock/vault_helper.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4535613601c419f7d20f0c21e638dabccf69b4a7fac99d5f6f9b81d1519dafd6", + "format": 1 + }, + { + "name": "tests/unit/mock/yaml_helper.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fada9f3506c951e21c60c2a0e68d3cdf3cadd71c8858b2d14a55c4b778f10983", + "format": 1 + }, + { + "name": "tests/unit/modules", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/modules/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/unit/modules/utils.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f69d69fb3ae2f709ff2470342f81bd8db3d7a03bc6296e099a0791d782faffb6", + "format": 1 + }, + { + "name": "tests/unit/plugins", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/lookup", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/lookup/fixtures", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/lookup/fixtures/avi.json", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3739de410d134591fada61f62053bfab6fcbd5c80fe2267faa7971f9fe36570d", + "format": 1 + }, + { + "name": "tests/unit/plugins/lookup/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/unit/plugins/lookup/test_laps_password.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d23071d213784b71a61661cc7c63ce4308c177ea8f758e3c53c14420675923f2", + "format": 1 + }, + { + "name": "tests/unit/plugins/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/unit/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/unit/conftest.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d53fa7efbf09b1639ac13de398a0bc326fdb0ee3d45643782d0b232d838cf277", + "format": 1 + }, + { + "name": "tests/unit/requirements.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fff3d6ecd2e8d31e2117523ebad45325c0f8e4b9c0e44af19acd3421012553f7", + "format": 1 + }, + { + "name": "tests/utils", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/utils/shippable", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/utils/shippable/sanity.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0583bfdf73fd78acd74f91917fb5ef6e9c49949a0868917624d0469bca4d9e08", + "format": 1 + }, + { + "name": "tests/utils/shippable/shippable.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "76c4bc006b7727d1e7769327498ba6ea55249f00c503a582bca2af5e2ba949c5", + "format": 1 + }, + { + "name": "tests/utils/shippable/units.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "56861f0a1bb5277793d5fa6b53a696e360069bf30d6aeac27d7b2d297965de6f", + "format": 1 + }, + { + "name": "tests/utils/shippable/windows.sh", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a4ba589649e222c8edece983838c0601cf3309505cfb243a8461add3066c8e24", + "format": 1 + }, + { + "name": "tests/.gitignore", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b5726d3ec9335a09c124469eca039523847a6b0f08a083efaefd002b83326600", + "format": 1 + }, + { + "name": "tests/requirements.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bd88d210dd7925cf3eb92089130a1b224d03d9b5819460d8f5c2730e36b660b2", + "format": 1 + }, + { + "name": ".git-blame-ignore-revs", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b4e007a74ae96df98a7c1aa868058a050ab7e757377fc70fd9e2613bae41be67", + "format": 1 + }, + { + "name": ".gitignore", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "78a0dfba0e64efbf2f0962b9cd386cf211935916b41a50860febb51f60242122", + "format": 1 + }, + { + "name": "CHANGELOG.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d7402c38ff658706bd14d6c7db15c7b50fbcafad51d03d4ef81ea4c2094cf20c", + "format": 1 + }, + { + "name": "COPYING", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0ae0485a5bd37a63e63603596417e4eb0e653334fa6c7f932ca3a0e85d4af227", + "format": 1 + }, + { + "name": "README.md", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d41e4c3d8b7699c6d9770a9824f75094b45c3cf933a76fcdfa1d6736ba33a3d3", + "format": 1 + } + ], + "format": 1 +}
\ No newline at end of file diff --git a/ansible_collections/community/windows/MANIFEST.json b/ansible_collections/community/windows/MANIFEST.json new file mode 100644 index 000000000..9ccb7a7c3 --- /dev/null +++ b/ansible_collections/community/windows/MANIFEST.json @@ -0,0 +1,33 @@ +{ + "collection_info": { + "namespace": "community", + "name": "windows", + "version": "1.13.0", + "authors": [ + "Jordan Borean @jborean93", + "Matt Davis @nitzmahone" + ], + "readme": "README.md", + "tags": [ + "windows" + ], + "description": "Ansible collection for community Windows plugins.\n", + "license": [], + "license_file": "COPYING", + "dependencies": { + "ansible.windows": ">=0.0.0,<2.0.0" + }, + "repository": "https://github.com/ansible-collections/community.windows", + "documentation": "https://docs.ansible.com/ansible/devel/collections/community/windows", + "homepage": "https://github.com/ansible-collections/community.windows", + "issues": "https://github.com/ansible-collections/community.windows/issues" + }, + "file_manifest_file": { + "name": "FILES.json", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3052b173ce376c2ede2b3307a6074b34d1bf10f772e8941fca335477e66958b8", + "format": 1 + }, + "format": 1 +}
\ No newline at end of file diff --git a/ansible_collections/community/windows/README.md b/ansible_collections/community/windows/README.md new file mode 100644 index 000000000..895999ee2 --- /dev/null +++ b/ansible_collections/community/windows/README.md @@ -0,0 +1,105 @@ +# Ansible Collection: community.windows + +[![Build Status](https://dev.azure.com/ansible/community.windows/_apis/build/status/CI?branchName=main)](https://dev.azure.com/ansible/community.windows/_build/latest?definitionId=23&branchName=main) +[![codecov](https://codecov.io/gh/ansible-collections/community.windows/branch/main/graph/badge.svg)](https://codecov.io/gh/ansible-collections/community.windows) + + +The `community.windows` collection includes the community plugins supported by Ansible community to help the management of Windows hosts. + +## Ansible version compatibility + +This collection has been tested against following Ansible versions: **>=2.12**. + +Plugins and modules within a collection may be tested with only specific Ansible versions. +A collection may contain metadata that identifies these versions. +PEP440 is the schema used to describe the versions of Ansible. + +## Collection Documentation + +Browsing the [**latest** collection documentation](https://docs.ansible.com/ansible/latest/collections/community/windows) will show docs for the _latest version released in the Ansible package_ not the latest version of the collection released on Galaxy. + +Browsing the [**devel** collection documentation](https://docs.ansible.com/ansible/devel/collections/community/windows) shows docs for the _latest version released on Galaxy_. + +We also separately publish [**latest commit** collection documentation](https://ansible-collections.github.io/community.windows/branch/main/) which shows docs for the _latest commit in the `main` branch_. + +If you use the Ansible package and don't update collections independently, use **latest**, if you install or update this collection directly from Galaxy, use **devel**. If you are looking to contribute, use **latest commit**. + +## Installation and Usage + +### Installing the Collection from Ansible Galaxy + +Before using the Windows collection, you need to install it with the `ansible-galaxy` CLI: + + ansible-galaxy collection install community.windows + +You can also include it in a `requirements.yml` file and install it via `ansible-galaxy collection install -r requirements.yml` using the format: + +```yaml +collections: +- name: community.windows +``` + + +## Contributing to this collection + +Currently we welcome bugfixes or feature requests to plugins in this collection but no new modules or plugins will be accepted in this collection. If you find problems, please open an issue or create a PR against the [Community Windows collection repository](https://github.com/ansible-collections/community.windows). See [Contributing to Ansible-maintained collections](https://docs.ansible.com/ansible/devel/community/contributing_maintained_collections.html#contributing-maintained-collections) for details. + +See [Developing modules for Windows](https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_general_windows.html#developing-modules-general-windows) for specifics on Windows modules. + +You can also join us on: + +IRC - ``#ansible-windows`` [irc.libera.chat](https://libera.chat/) channel + +See the [Ansible Community Guide](https://docs.ansible.com/ansible/latest/community/index.html) for details on contributing to Ansible. + + +### Code of Conduct +This collection follows the Ansible project's +[Code of Conduct](https://docs.ansible.com/ansible/devel/community/code_of_conduct.html). +Please read and familiarize yourself with this document. + + +### Testing with `ansible-test` + +The `tests` directory contains configuration for running sanity and integration tests using [`ansible-test`](https://docs.ansible.com/ansible/latest/dev_guide/testing_integration.html). + +You can run the collection's test suites with the commands: + + ansible-test sanity --docker + ansible-test windows-integration --docker + + +## Publishing New Version + +The current process for publishing new versions of the Windows Core Collection is manual, and requires a user who has access to the `community` namespace on Ansible Galaxy and Automation Hub to publish the build artifact. + +* Update `galaxy.yml` with the new version for the collection. +* Update the `CHANGELOG`: + * Make sure you have [`antsibull-changelog`](https://pypi.org/project/antsibull-changelog/) installed `pip install antsibull-changelog`. + * Make sure there are fragments for all known changes in `changelogs/fragments`. + * Add a new fragment with the header `release_summary` to give a summary on the release. + * Run `antsibull-changelog release`. +* Commit the changes and wait for CI to be green +* Create a release with the tag that matches the version number + * The tag is the version number itself, and should not start with anything + * This will trigger a build and publish the collection to Galaxy + * The Zuul job progress will be listed [here](https://ansible.softwarefactory-project.io/zuul/builds?project=ansible-collections%2Fcommunity.windows&skip=0) + +After the version is published, verify it exists on the [Windows Community Collection Galaxy page](https://galaxy.ansible.com/community/windows). + + +## More Information + +For more information about Ansible's Windows integration, join the `#ansible-windows` channel on [libera.chat](https://libera.chat/) IRC, and browse the resources in the [Windows Working Group](https://github.com/ansible/community/wiki/Windows) Community wiki page. + +- [Ansible Collection overview](https://github.com/ansible-collections/overview) +- [Ansible User guide](https://docs.ansible.com/ansible/latest/user_guide/index.html) +- [Ansible Developer guide](https://docs.ansible.com/ansible/latest/dev_guide/index.html) +- [Ansible Community code of conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html) + + +## License + +GNU General Public License v3.0 or later + +See [COPYING](COPYING) to see the full text. diff --git a/ansible_collections/community/windows/changelogs/changelog.yaml b/ansible_collections/community/windows/changelogs/changelog.yaml new file mode 100644 index 000000000..d64c61b79 --- /dev/null +++ b/ansible_collections/community/windows/changelogs/changelog.yaml @@ -0,0 +1,464 @@ +ancestor: null +releases: + 0.2.0: + changes: + bugfixes: + - '**security issue** win_unzip - normalize paths in archive to ensure extracted + files do not escape from the target directory (CVE-2020-1737) + + ' + - psexec - Fix issue where the Kerberos package was not detected as being available. + - psexec - Fix issue where the ``interactive`` option was not being passed down + to the library. + - win_credential - Fix issue that errors when trying to add a ``name`` with + wildcards. + - win_domain_computer - Fix idempotence checks when ``sAMAccountName`` is different + from ``name`` + - win_domain_computer - Honour the explicit domain server and credentials when + moving or removing a computer object - https://github.com/ansible/ansible/pull/63093 + - win_domain_user - Better handle cases when getting a new user's groups fail + - https://github.com/ansible/ansible/issues/54331 + - win_format - Idem not working if file exist but same fs (https://github.com/ansible/ansible/issues/58302) + - win_format - fixed issue where module would not change allocation unit size + (https://github.com/ansible/ansible/issues/56961) + - win_iis_webapppool - Do not try and set attributes in check mode when the + pool did not exist + - win_iis_website - Actually restart the site when ``state=restarted`` - https://github.com/ansible/ansible/issues/63828 + - win_partition - Fix invalid variable name causing a failure on checks - https://github.com/ansible/ansible/issues/62401 + - win_partition - don't resize partitions if size difference is < 1 MiB + - win_timezone - Allow for _dstoff timezones + - win_unzip - Fix support for paths with square brackets not being detected + properly + minor_changes: + - win_disk_facts - Set output array order to be by disk number property - https://github.com/ansible/ansible/issues/63998 + - win_domain_computer - ``sam_account_name`` with missing ``$`` will have it + added automatically (https://github.com/ansible-collections/community.windows/pull/93) + - win_domain_computer - add support for offline domain join (https://github.com/ansible-collections/community.windows/pull/93) + - win_domain_group_membership - Add multi-domain forest support - https://github.com/ansible/ansible/issues/59829 + - win_domain_user - Added the ``identity`` module option to explicitly set the + identity of the user when searching for it - https://github.com/ansible/ansible/issues/45298 + - win_firewall- Change req check from wmf version to cmdlets presence - https://github.com/ansible/ansible/issues/63003 + - win_firewall_rule - add parameter to support ICMP Types and Codes (https://github.com/ansible/ansible/issues/46809) + - win_iis_webapplication - add new options ``connect_as``, ``username``, ``password``. + - win_iis_webapplication - now uses the current application pool of the website + instead of the DefaultAppPool if none was specified. + - win_nssm - Implement additional parameters - (https://github.com/ansible/ansible/issues/62620) + - win_pester - Only execute ``*.tests.ps1`` in ``path`` to match the default + behaviour in Pester - https://github.com/ansible/ansible/issues/55736 + release_summary: 'This is the first proper release of the ``community.windows`` + collection on 2020-07-18. + + The changelog describes all changes made to the modules and plugins included + in this collection since Ansible 2.9.0. + + ' + removed_features: + - win_disk_image - removed the deprecated return value ``mount_path`` in favour + of ``mount_paths``. + fragments: + - 56033-win_iis_webapplication-add-authentication-options.yml + - 56966-win_format-allocation-unit-size.yml + - 58225-win_partition-maximum-partition-size.yml + - 61227-win_iis_webapplication-apppool-change.yml + - 65138-Windows_Multidomain_support.yml + - 93-win_domain_computer-offline-domain-join.yml + - psexec-kerb-and-interactive.yaml + - summary-0.2.0.yml + - win-unzip-check-extraction-path.yml + - win_credential-wildcard.yaml + - win_disk_facts-Set-output-array-order-by-disk-number.yml + - win_disk_image_mount_path.yml + - win_domain_computer-credential.yaml + - win_domain_computer-idempotence.yaml + - win_domain_user-group-missing.yaml + - win_domain_user-identity.yaml + - win_firewall-Change-req-check-from-wmf-version-to-cmdlets-presence.yml + - win_firewall_rule-add-support-for-icmptypecode.yml + - win_format-Idem-not-working-if-file-exist-but-same-fs.yml + - win_iis_webapppool-check-mode.yaml + - win_iis_website-restarted.yaml + - win_nssm-Implement-additional-parameters.yml + - win_partition-var.yaml + - win_pester-path-behaviour.yaml + - win_timezone-Allow-dstoff.yml + - win_unzip-paths.yaml + release_date: '2020-07-18' + 1.0.0: + changes: + breaking_changes: + - win_pester - no longer runs all ``*.ps1`` file in the directory specified + due to it executing potentially unknown scripts. It will follow the default + behaviour of only running tests for files that are like ``*.tests.ps1`` which + is built into Pester itself. + bugfixes: + - win_scoop - add checks for globally installed packages for better idempotency + checks + minor_changes: + - win_dns_record - Added support for managing ``SRV`` records + - win_firewall_rule - Support editing rules by the group it belongs to + - win_firewall_rule - Support editing rules that have a duplicated name + removed_features: + - win_psexec - removed the deprecated ``extra_opts`` option. + fragments: + - 1.0.0-summary.yml + - porting-guide.yml + - win_dns_record_SRV.yml + - win_firewall_rule_group_state.yml + - win_scoop.yml + modules: + - description: Manage Scoop buckets + name: win_scoop_bucket + namespace: '' + release_date: '2020-08-14' + 1.1.0: + changes: + minor_changes: + - win_dns_record - Support NS record creation,modification and deletion + - win_firewall - Support defining the default inbound and outbound action of + traffic in Windows firewall. + - win_psrepository - Added the ``proxy`` option that defines the proxy to use + for the repository being managed + fragments: + - v1.1.0-summary.yml + - win_dns_ns_record.yml + - win_firewall_action.yml + - win_psrepository-proxy.yml + release_date: '2020-09-26' + 1.10.0: + changes: + bugfixes: + - win_hotfix - Supports hotfixes that contain multiple updates inside the supplied + update msu - https://github.com/ansible-collections/community.windows/issues/284 + - win_iis_webapplication - Fix physical path check for broken configurations + - https://github.com/ansible-collections/community.windows/pull/385 + - win_rds_cap - Fix SID lookup with any account ending with the ``@builtin`` + UPN suffix + - win_rds_rap - Fix SID lookup with any account ending with the ``@builtin`` + UPN suffix + - win_region - Fix junk output when copying settings across users + - win_scoop - Fix bootstrapping process to properly work when running as admin + - win_scoop_bucket - Fix handling of output and errors from each scoop command + minor_changes: + - win_domain_user - Add support for managing service prinicpal names via the + ``spn`` param and principals allowed to delegate via the ``delegates`` param + (https://github.com/ansible-collections/community.windows/pull/365) + - win_domain_user - Added the ``groups_missing_behaviour`` option that controls + the behaviour when a group specified does not exist - https://github.com/ansible-collections/community.windows/pull/375 + - win_hotfix - Added the ``identifiers`` and ``kbs`` return value that is always + a list of identifiers and kbs inside a hotfix + - win_psmodule - Add credential support for through the ``username`` and ``password`` + options + - win_psrepository - Add credential support for through the ``username`` and + ``password`` options + release_summary: Release summary for v1.10.0 + fragments: + - 365-win_domain_user-add-support-for-spns-kerberos.yml + - v1.10.0-release.yml + - win_domain_user-groups-missing.yml + - win_hotfix-multiple-msu.yml + - win_iis_webapplication-missing-path.yml + - win_psmodule-credentials.yml + - win_rds-sid.yml + - win_region-output.yml + - win_scoop-install.yml + - win_scoop_bucket-error.yml + modules: + - description: Recopilates the facts of the listening ports of the machine + name: win_listen_ports_facts + namespace: '' + release_date: '2022-05-13' + 1.11.0: + changes: + bugfixes: + - win_domain_user - Fix broken warning call when failing to get group membership + - https://github.com/ansible-collections/community.windows/issues/412 + - win_scheduled_task - Fix the Monthly DOW trigger value ``run_on_last_week_of_month`` + when ``weeks_of_month`` is also set - https://github.com/ansible-collections/community.windows/issues/414 + minor_changes: + - Raise minimum Ansible version to ``2.11`` or newer + - win_psmodule module - add ``accept_license`` option to allow for installing + modules that require license acceptance (https://github.com/ansible-collections/community.windows/issues/340). + release_summary: Release summary for v1.11.0 + fragments: + - 1.11.0-release.yml + - 340-win_psmodule-accept_license-option.yml + - ansible-version-bump.yml + - win_domain_user-add-warning.yml + - win_scheduled_task_week-trigger.yml + release_date: '2022-08-11' + 1.11.1: + changes: + bugfixes: + - win_dhcp_lease - call Get-DhcpServerv4Lease once when MAC and IP are defined + (https://github.com/ansible-collections/community.windows/pull/427) + - win_dhcp_lease - fix mac address convert (https://github.com/ansible-collections/community.windows/issues/291) + - win_psmodule - Fix bootstrapping PowerShellGet with ``-AcceptLicense`` - https://github.com/ansible-collections/community.windows/issues/424 + - win_psmodule - Source PowerShellGet and PackagementManagement from ``repository`` + if specified + - win_region - did not allow regional format en-150 (= English(Europe); also + referred as en-EU or en-Europe). This fix allows specifying en-150 as regional + format (https://github.com/ansible-collections/community.windows/issues/438). + - win_scoop - Fix idempotency checks with Scoop ``v0.2.3`` and newer. + release_summary: Release summary for v1.11.1 + fragments: + - 427-win_dhcp_lease-improvements.yml + - 439-win_region-allow-all-cultures.yaml + - scoop-export.yml + - v1.11.1-release.yml + - win_psmodule-bootstrapping.yml + release_date: '2022-11-02' + 1.12.0: + changes: + bugfixes: + - win_firewall_rule - fix problem in check mode with multiple ip addresses not + in same order + - win_partition - fix problem in auto assigning a drive letter should the user + use either a, u, t or o as a drive letter + minor_changes: + - win_dns_record - Added support for DHCID (RFC 4701) records + - win_domain_user - Added the ``display_name`` option to set the users display + name attribute + release_summary: Release summary for v1.12.0 + fragments: + - 440-win_firewall_rule.yml + - 463-fix_win_partition_auto_assign.yaml + - release-summary.yml + - win_dns_record-dhcid-support.yaml + - win_domain_user-display_name.yml + release_date: '2022-12-20' + 1.13.0: + changes: + bugfixes: + - win_disk_facts - Fix issue when enumerating non-physical disks or disks without + numbers - https://github.com/ansible-collections/community.windows/issues/474 + - win_firewall_rule - fix program cannot be set to any on existing rules. + - win_psmodule - Fix missing AcceptLicense parameter that occurs when the pre-reqs + have been installed - https://github.com/ansible-collections/community.windows/issues/487 + - 'win_pssession_configuration - Fix parser error (Invalid JSON primitive: icrosoft.WSMan.Management.WSManConfigContainerElement)' + - win_xml - Fixes the issue when no childnode is defined and will allow adding + a new element to an empty element. + - win_zip - fix source appears to use backslashes as path separators issue when + extracting Zip archve in non-Windows environment - https://github.com/ansible-collections/community.windows/issues/442 + minor_changes: + - Raise minimum Ansible version to ``2.12`` or newer + - win_dns_record - Add parameter ``aging`` for creating non-static DNS records. + - win_domain_computer - Add ActiveDirectory module import + - win_domain_object_info - Add ActiveDirectory module import + - win_psmodule - add ``force`` option to allow overwriting/updating existing + module dependency only if requested + - win_pssession_configuration - Add diff mode support + release_summary: Release summary for v1.13.0 + fragments: + - 442-win_zip-backslash.yml + - 472-win_firewall_rule.yml + - 480-win_dns_record-aging.yaml + - 482-win_xml-no-childnode.yml + - 491-win_pssession_configuration.yml + - 508-win_psmodule-force.yml + - 509-import-activedirectory-module.yml + - ansible-min.yml + - release-1.13.0.yml + - win_disk_facts-non-physical-disk.yml + - win_psmodule-prereqs.yml + release_date: '2023-05-02' + 1.2.0: + changes: + bugfixes: + - win_partition - fix size comparison errors when size specified in bytes (https://github.com/ansible-collections/community.windows/pull/159) + - win_security_policy - read config file with correct encoding to avoid breaking + non-ASCII chars + - win_security_policy - strip of null char added by secedit for ``LegalNoticeText`` + so the existing value is preserved + minor_changes: + - win_nssm - added new parameter 'app_environment' for managing service environment. + - win_scheduled_task - validate task name against invalid characters (https://github.com/ansible-collections/community.windows/pull/168) + - win_scheduled_task_stat - add check mode support (https://github.com/ansible-collections/community.windows/pull/167) + fragments: + - 158-nssm-appenv-and-custom-pathvar.yaml + - 159-win_partition_bytes_as_number.yaml + - 167-win_scheduled_task_stat-check_mode.yml + - 168-win_scheduled_task-invalid-task-name.yml + - release_summary.yml + - win_security_policy_encoding.yml + modules: + - description: Enable or disable certain network adapters. + name: win_net_adapter_feature + namespace: '' + release_date: '2020-12-18' + 1.3.0: + changes: + bugfixes: + - 'win_firewall_rule - Ensure ``service: any`` is set to match any service instead + of the literal service called ``any`` as per the docs' + - win_scoop - Make sure we enable TLS 1.2 when installing scoop + - win_xml - Fix ``PropertyNotFound`` exception when creating a new attribute + - https://github.com/ansible-collections/community.windows/issues/166 + minor_changes: + - Extend win_domain_computer adding managedBy parameter. + fragments: + - 185-add-managedBy-support.yml + - release_summary.yml + - win_firewall_rule-service-any.yml + - win_scoop-tls.yml + - win_xml-missing-attribute.yml + modules: + - description: Copies registered PSRepositories to other user profiles + name: win_psrepository_copy + namespace: '' + release_date: '2021-02-03' + 1.4.0: + changes: + bugfixes: + - win_domain_group_membership - Handle timeouts when dealing with group with + lots of members - https://github.com/ansible-collections/community.windows/pull/204 + - win_domain_user - Make sure a password is set to change when it is marked + as password needs to be changed before logging in - https://github.com/ansible-collections/community.windows/issues/223 + - win_domain_user - fix reporting on user when running in check mode - https://github.com/ansible-collections/community.windows/pull/248 + - win_lineinfile - Fix crash when using ``insertbefore`` and ``insertafter`` + at the same time - https://github.com/ansible-collections/community.windows/issues/220 + - win_partition - Fix gtp_type setting in win_partition - https://github.com/ansible-collections/community.windows/issues/241 + - win_psmodule - Makes sure ``-AllowClobber`` is used when updating pre-requisites + if requested - https://github.com/ansible-collections/community.windows/issues/42 + - win_pssession_configuration - the ``async_poll`` option was not actually used + and polling mode was always used with the default poll delay; this change + also formally disables ``async_poll=0`` (https://github.com/ansible-collections/community.windows/pull/212). + - win_wait_for_process - Fix bug when specifying multiple ``process_name_exact`` + values - https://github.com/ansible-collections/community.windows/issues/203 + fragments: + - 212-win_pssession_configuration-async_poll.yml + - release-summary.yml + - win_domain_group_membership-large.yml + - win_domain_user-check-reporting.yml + - win_domain_user-pass-change.yaml + - win_lineinfile-warning.yaml + - win_partition-gpttype.yml + - win_psmodule-prereq.yml + - win_wait_for_process-exact.yml + modules: + - description: Gather information about Windows features + name: win_feature_info + namespace: '' + release_date: '2021-05-15' + 1.5.0: + changes: + bugfixes: + - win_dns_zone - Fix idempotency when using a DNS zone with forwarders - https://github.com/ansible-collections/community.windows/issues/259 + - win_domain_group_member - Fix faulty logic when comparing existing group members + - https://github.com/ansible-collections/community.windows/issues/256 + - win_lineinfile - Avoid stripping the newline at the end of a file - https://github.com/ansible-collections/community.windows/pull/219 + - win_product_facts - fixed an issue that the module doesn't correctly convert + a product id (https://github.com/ansible-collections/community.windows/pull/251). + fragments: + - 251-win_product_facts.yml + - release-summary.yml + - win_dns_zone-forwarder.yml + - win_domain_group_membership-comparison.yml + - win_lineinfile-newline.yml + release_date: '2021-06-26' + 1.6.0: + changes: + minor_changes: + - win_dns_record - Added txt Support + - win_scheduled_task - Added support for setting a ``session_state_change`` + trigger by documenting the human friendly values for ``state_change`` + - win_scheduled_task_state - Added ``state_change_str`` to the trigger output + to give a human readable description of the value + security_fixes: + - win_psexec - Ensure password is masked in ``psexec_command`` return result + - https://github.com/ansible-collections/community.windows/issues/43 + fragments: + - release_summary.yml + - scheduled_task_state.yml + - win_dns_record-txtsupport.yml + - win_psexec-cmd-output.yml + release_date: '2021-07-27' + 1.7.0: + changes: + bugfixes: + - win_dns_record - Fix issue when trying to use the ``computer_name`` option + - https://github.com/ansible-collections/community.windows/issues/276 + - win_domain_user - Fallback to NETBIOS username for password verification check + if the UPN is not set - https://github.com/ansible-collections/community.windows/pull/289 + - 'win_initialize_disk - Ensure ``online: False`` doesn''t bring the disk online + again - https://github.com/ansible-collections/community.windows/pull/268' + - win_lineinfile - Fix up diff output with ending newlines - https://github.com/ansible-collections/community.windows/pull/283 + - win_region - Fix ``copy_settings`` on a host that has disabled ``reg.exe`` + access - https://github.com/ansible-collections/community.windows/issues/287 + minor_changes: + - win_domain_user - Added ``sam_account_name`` to explicitly set the ``sAMAccountName`` + property of an object - https://github.com/ansible-collections/community.windows/issues/281 + fragments: + - 1.7.0-release.yml + - win_dns_record-comp-name.yml + - win_domain_user-missing-upn.yml + - win_domain_user_samaccount.yml + - win_initialize_disk-offline.yml + - win_lineinfile-diff.yml + - win_region_hardened_registry.yml + release_date: '2021-09-23' + 1.8.0: + changes: + bugfixes: + - win_audit_rule - Fix exception when trying to change a rule on a hidden or + protected system file - https://github.com/ansible-collections/community.windows/issues/17 + - win_firewall - Fix GpoBoolean/Boolean comparation(windows versions compatibility + increase) + - win_nssm - Perform better user comparison checks for idempotency + - win_pssession_configuration - the associated action plugin detects check mode + using a method that isn't always accurate (https://github.com/ansible-collections/community.windows/pull/318). + - win_region - Fix conflicts with existing ``LIB`` environment variable + - win_scheduled_task - Fix conflicts with existing ``LIB`` environment variable + - win_scheduled_task_stat - Fix conflicts with existing ``LIB`` environment + variable + - win_scoop_bucket - Ensure no extra data is sent to the controller resulting + in a junk output warning + - win_xml - Do not show warnings for normal operations - https://github.com/ansible-collections/community.windows/issues/205 + - win_xml - Fix removal operation when running with higher verbosities - https://github.com/ansible-collections/community.windows/issues/275 + minor_changes: + - win_nssm - Added ``username`` as an alias for ``user`` + - win_nssm - Remove deprecation for ``state``, ``dependencies``, ``user``, ``password``, + ``start_mode`` + - win_nssm - Support gMSA accounts for ``user`` + fragments: + - 205-win_xml-warning.yml + - 275-win_xml-verbose-removed.yml + - 318-win_pssession_configuration-action-check_mode.yml + - add-type-env.yml + - release.yml + - win_audit_rule-protected-file.yml + - win_firewall_bools_comparation_compatibility.yml + - win_nssm-username.yml + - win_scoop_bucket-junk.yml + modules: + - description: Manage Active Directory Organizational Units + name: win_domain_ou + namespace: '' + release_date: '2021-11-03' + 1.9.0: + changes: + bugfixes: + - win_domain_user - Module now properly captures and reports bad password - + https://github.com/ansible-collections/community.windows/issues/316 + - win_domain_user - Module now reports user created and changed properly - https://github.com/ansible-collections/community.windows/issues/316 + - win_domain_user - The AD user's existing identity is searched using their + sAMAccountName name preferentially and falls back to the provided name property + instead - https://github.com/ansible-collections/community.windows/issues/344 + - win_iis_virtualdirectory - Fixed an issue where virtual directory information + could not be obtained correctly when the parameter ``application`` was set + minor_changes: + - win_disk_facts - Added ``filter`` option to filter returned facts by type + of disk information - https://github.com/ansible-collections/community.windows/issues/33 + - win_disk_facts - Converted from ``#Requires -Module Ansible.ModuleUtils.Legacy`` + to ``#AnsibleRequires -CSharpUtil Ansible.Basic`` + - win_iis_virtualdirectory - Added the ``connect_as``, ``username``, and ``password`` + options to control the virtual directory authentication - https://github.com/ansible-collections/community.windows/issues/346 + - win_power_plan - Added ``guid`` option to specify plan by a unique identifier + - https://github.com/ansible-collections/community.windows/issues/310 + fragments: + - 310_win_disk_facts.yml + - 330_win_domain_user.yml + - 337_win_power_plan.yml + - 345_win_domain_user.yml + - 347_win_iis_virtualdirectory.yml + - release-2.9.0.yml + release_date: '2021-12-21' diff --git a/ansible_collections/community/windows/changelogs/config.yaml b/ansible_collections/community/windows/changelogs/config.yaml new file mode 100644 index 000000000..78e36a2b4 --- /dev/null +++ b/ansible_collections/community/windows/changelogs/config.yaml @@ -0,0 +1,29 @@ +changelog_filename_template: ../CHANGELOG.rst +changelog_filename_version_depth: 0 +changes_file: changelog.yaml +changes_format: combined +keep_fragments: false +mention_ancestor: true +new_plugins_after_name: removed_features +notesdir: fragments +prelude_section_name: release_summary +prelude_section_title: Release Summary +sections: +- - major_changes + - Major Changes +- - minor_changes + - Minor Changes +- - breaking_changes + - Breaking Changes / Porting Guide +- - deprecated_features + - Deprecated Features +- - removed_features + - Removed Features (previously deprecated) +- - security_fixes + - Security Fixes +- - bugfixes + - Bugfixes +- - known_issues + - Known Issues +title: Community Windows +trivial_section_name: trivial diff --git a/ansible_collections/community/windows/changelogs/fragments/.keep b/ansible_collections/community/windows/changelogs/fragments/.keep new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/community/windows/changelogs/fragments/.keep diff --git a/ansible_collections/community/windows/docs/docsite/links.yml b/ansible_collections/community/windows/docs/docsite/links.yml new file mode 100644 index 000000000..887f62037 --- /dev/null +++ b/ansible_collections/community/windows/docs/docsite/links.yml @@ -0,0 +1,41 @@ +--- +# based on https://github.com/ansible-collections/collection_template/blob/main/docs/docsite/links.yml +# +# This will make sure that plugin and module documentation gets Edit on GitHub links +# that allow users to directly create a PR for this plugin or module in GitHub's UI. +# Remove this section if the collection repository is not on GitHub, or if you do not want this +# functionality for your collection. +edit_on_github: + repository: ansible-collections/community.windows + branch: main + # If your collection root (the directory containing galaxy.yml) does not coincide with your + # repository's root, you have to specify the path to the collection root here. For example, + # if the collection root is in a subdirectory ansible_collections/community/REPO_NAME + # in your repository, you have to set path_prefix to 'ansible_collections/community/REPO_NAME'. + path_prefix: '' + +# Here you can add arbitrary extra links. Please keep the number of links down to a +# minimum! Also please keep the description short, since this will be the text put on +# a button. +# +# Also note that some links are automatically added from information in galaxy.yml. +# The following are automatically added: +# 1. A link to the issue tracker (if `issues` is specified); +# 2. A link to the homepage (if `homepage` is specified and does not equal the +# `documentation` or `repository` link); +# 3. A link to the collection's repository (if `repository` is specified). + +# extra_links: +# - description: <DESC> +# url: <URL> + +# Specify communication channels for your collection. We suggest to not specify more +# than one place for communication per communication tool to avoid confusion. +communication: + matrix_rooms: + - topic: General usage and support questions + room: '#windows:ansible.im' + irc_channels: + - topic: General usage and support questions + network: Libera + channel: '#ansible-windows' diff --git a/ansible_collections/community/windows/meta/runtime.yml b/ansible_collections/community/windows/meta/runtime.yml new file mode 100644 index 000000000..1f0649152 --- /dev/null +++ b/ansible_collections/community/windows/meta/runtime.yml @@ -0,0 +1 @@ +requires_ansible: '>=2.12' diff --git a/ansible_collections/community/windows/plugins/action/win_pssession_configuration.py b/ansible_collections/community/windows/plugins/action/win_pssession_configuration.py new file mode 100644 index 000000000..35234a68e --- /dev/null +++ b/ansible_collections/community/windows/plugins/action/win_pssession_configuration.py @@ -0,0 +1,146 @@ +# Copyright: (c) 2020, Brian Scholer <@briantist> +# 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 + +import time + +from ansible.errors import AnsibleError +from ansible.plugins.action import ActionBase +from ansible.playbook.task import Task +from ansible.utils.display import Display +display = Display() + + +def clean_async_result(reference_keys, obj): + for key in reference_keys: + obj.pop(key) + return obj + + +class ActionModule(ActionBase): + _default_async_timeout = 300 + _default_async_poll = 1 + + def __init__(self, *args, **kwargs): + super(ActionModule, self).__init__(*args, **kwargs) + + def run(self, tmp=None, task_vars=None): + self._supports_check_mode = True + self._supports_async = False + check_mode = self._task.check_mode + async_timeout = self._task.args.get('async_timeout', self._default_async_timeout) + async_poll = self._task.args.get('async_poll', self._default_async_poll) + + result = super(ActionModule, self).run(tmp, task_vars) + + if async_poll <= 0: + raise AnsibleError("The 'async_poll' option must be greater than 0, got: %i" % async_poll) + + # build the wait_for_connection object for later use + wait_for_connection_task = self._task.copy() + wait_for_connection_task.args = { + 'timeout': async_timeout, + 'sleep': async_poll, + } + wait_connection_action = self._shared_loader_obj.action_loader.get( + 'wait_for_connection', + task=wait_for_connection_task, + connection=self._connection, + play_context=self._play_context, + loader=self._loader, + templar=self._templar, + shared_loader_obj=self._shared_loader_obj + ) + + # if it's not in check mode, call the module async so the WinRM restart doesn't kill ansible + if not check_mode: + self._task.async_val = async_timeout + self._task.poll = async_poll + + result = status = self._execute_module( + task_vars=task_vars, + module_args=self._task.args + ) + display.vvvv("Internal Async Result: %r" % status) + + # if we're in check mode (not doing async) return the result now + if check_mode: + return result + + # turn off async so we don't run the following actions as async + self._task.async_val = 0 + + # build the async_status object + async_status_load_params = dict( + action='async_status jid=%s' % status['ansible_job_id'], + environment=self._task.environment + ) + async_status_task = Task().load(async_status_load_params) + async_status_action = self._shared_loader_obj.action_loader.get( + 'async_status', + task=async_status_task, + connection=self._connection, + play_context=self._play_context, + loader=self._loader, + templar=self._templar, + shared_loader_obj=self._shared_loader_obj + ) + + # build an async_status mode=cleanup object + async_cleanup_load_params = dict( + action='async_status mode=cleanup jid=%s' % status['ansible_job_id'], + environment=self._task.environment + ) + async_cleanup_task = Task().load(async_cleanup_load_params) + async_cleanup_action = self._shared_loader_obj.action_loader.get( + 'async_status', + task=async_cleanup_task, + connection=self._connection, + play_context=self._play_context, + loader=self._loader, + templar=self._templar, + shared_loader_obj=self._shared_loader_obj + ) + + # Retries here is a fallback in case the module fails in an unexpected way + # which can sometimes not properly set the failed field in the return. + # It is not related to async retries. + # Without this, that situation would cause an infinite loop. + max_retries = 3 + retries = 0 + while not check_mode: + try: + # check up on the async job + job_status = async_status_action.run(task_vars=task_vars) + display.vvvv("Async Job Status: %r" % job_status) + + if job_status.get('failed', False): + raise AnsibleError(job_status.get('msg', job_status)) + + if job_status.get('finished', False): + result = job_status + break + + time.sleep(self._task.poll) + + except BaseException as e: + retries += 1 + if retries >= max_retries: + display.vvvv("Max retries reached.") + raise e + display.vvvv("Retrying (%s of %s)" % (retries, max_retries)) + display.vvvv("Falling back to wait_for_connection: %r" % e) + wait_connection_action.run(task_vars=task_vars) + + try: + # let's try to clean up after our implicit async + job_status = async_cleanup_action.run(task_vars=task_vars) + if job_status.get('failed', False): + display.vvvv("Clean up of async status failed on the remote host: %r" % job_status.get('msg', job_status)) + except BaseException as e: + # let's swallow errors during implicit cleanup to aovid interrupting what was otherwise a successful run + display.vvvv("Clean up of async status failed on the remote host: %r" % e) + + return clean_async_result(status.keys(), result) diff --git a/ansible_collections/community/windows/plugins/lookup/__init__.py b/ansible_collections/community/windows/plugins/lookup/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/community/windows/plugins/lookup/__init__.py diff --git a/ansible_collections/community/windows/plugins/lookup/laps_password.py b/ansible_collections/community/windows/plugins/lookup/laps_password.py new file mode 100644 index 000000000..ca0fce81c --- /dev/null +++ b/ansible_collections/community/windows/plugins/lookup/laps_password.py @@ -0,0 +1,357 @@ +# (c) 2019 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 = ''' +name: laps_password +author: Jordan Borean (@jborean93) +short_description: Retrieves the LAPS password for a server. +description: +- This lookup returns the LAPS password set for a server from the Active Directory database. +- See U(https://github.com/jborean93/ansible-lookup-laps_password) for more information around installing + pre-requisites and testing. +options: + _terms: + description: + - The host name to retrieve the LAPS password for. + - This is the C(Common Name (CN)) of the host. + required: True + type: str + allow_plaintext: + description: + - When set to C(yes), will allow traffic to be sent unencrypted. + - It is highly recommended to not touch this to avoid any credentials being exposed over the network. + - Use C(scheme=ldaps), C(auth=gssapi), or C(start_tls=yes) to ensure the traffic is encrypted. + default: no + type: bool + auth: + description: + - The type of authentication to use when connecting to the Active Directory server + - When using C(simple), the I(username) and I(password) options must be set. If not using C(scheme=ldaps) or + C(start_tls=True) then these credentials are exposed in plaintext in the network traffic. + - It is recommended ot use C(gssapi) as it will encrypt the traffic automatically. + - When using C(gssapi), run C(kinit) before running Ansible to get a valid Kerberos ticket. + - You cannot use C(gssapi) when either C(scheme=ldaps) or C(start_tls=True) is set. + choices: + - simple + - gssapi + default: gssapi + type: str + ca_cert: + description: + - The path to a CA certificate PEM file to use for certificate validation. + - Certificate validation is used when C(scheme=ldaps) or C(start_tls=yes). + - This may fail on hosts with an older OpenLDAP install like MacOS, this will have to be updated before + reinstalling python-ldap to get working again. + type: str + aliases: [ cacert_file ] + domain: + description: + - The domain to search in to retrieve the LAPS password. + - This could either be a Windows domain name visible to the Ansible controller from DNS or a specific domain + controller FQDN. + - Supports either just the domain/host name or an explicit LDAP URI with the domain/host already filled in. + - If the URI is set, I(port) and I(scheme) are ignored. + required: True + type: str + password: + description: + - The password for C(username). + - Required when C(username) is set. + type: str + port: + description: + - The LDAP port to communicate over. + - If I(kdc) is already an LDAP URI then this is ignored. + type: int + scheme: + description: + - The LDAP scheme to use. + - When using C(ldap), it is recommended to set C(auth=gssapi), or C(start_tls=yes), otherwise traffic will be in + plaintext. + - The Active Directory host must be configured for C(ldaps) with a certificate before it can be used. + - If I(kdc) is already an LDAP URI then this is ignored. + choices: + - ldap + - ldaps + default: ldap + search_base: + description: + - Changes the search base used when searching for the host in Active Directory. + - Will default to search in the C(defaultNamingContext) of the Active Directory server. + - If multiple matches are found then a more explicit search_base is required so only 1 host is found. + - If searching a larger Active Directory database, it is recommended to narrow the search_base for performance + reasons. + type: str + start_tls: + description: + - When C(scheme=ldap), will use the StartTLS extension to encrypt traffic sent over the wire. + - This requires the Active Directory to be set up with a certificate that supports StartTLS. + - This is ignored when C(scheme=ldaps) as the traffic is already encrypted. + type: bool + default: no + username: + description: + - Required when using C(auth=simple). + - The username to authenticate with. + - Recommended to use the username in the UPN format, e.g. C(username@DOMAIN.COM). + - This is required when C(auth=simple) and is not supported when C(auth=gssapi). + - Call C(kinit) outside of Ansible if C(auth=gssapi) is required. + type: str + validate_certs: + description: + - When using C(scheme=ldaps) or C(start_tls=yes), this controls the certificate validation behaviour. + - C(demand) will fail if no certificate or an invalid certificate is provided. + - C(try) will fail for invalid certificates but will continue if no certificate is provided. + - C(allow) will request and check a certificate but will continue even if it is invalid. + - C(never) will not request a certificate from the server so no validation occurs. + default: demand + choices: + - never + - allow + - try + - demand + type: str +requirements: +- python-ldap +notes: +- If a host was found but had no LAPS password attribute C(ms-Mcs-AdmPwd), the lookup will fail. +- Due to the sensitive nature of the data travelling across the network, it is highly recommended to run with either + C(auth=gssapi), C(scheme=ldaps), or C(start_tls=yes). +- Failing to run with one of the above settings will result in the account credentials as well as the LAPS password to + be sent in plaintext. +- Some scenarios may not work when running on a host with an older OpenLDAP install like MacOS. It is recommended to + install the latest OpenLDAP version and build python-ldap against this, see + U(https://keathmilligan.net/python-ldap-and-macos) for more information. +''' + +EXAMPLES = """ +# This isn't mandatory but it is a way to call kinit from within Ansible before calling the lookup +- name: call kinit to retrieve Kerberos token + expect: + command: kinit username@ANSIBLE.COM + responses: + (?i)password: SecretPass1 + no_log: True + +- name: Get the LAPS password using Kerberos auth, relies on kinit already being called + set_fact: + ansible_password: "{{ lookup('community.windows.laps_password', 'SERVER', domain='dc01.ansible.com') }}" + +- name: Specific the domain host using an explicit LDAP URI + set_fact: + ansible_password: "{{ lookup('community.windows.laps_password', 'SERVER', domain='ldap://ansible.com:389') }}" + +- name: Use Simple auth over LDAPS + set_fact: + ansible_password: "{{ lookup('community.windows.laps_password', 'server', + domain='dc01.ansible.com', + auth='simple', + scheme='ldaps', + username='username@ANSIBLE.COM', + password='SuperSecret123') }}" + +- name: Use Simple auth with LDAP and StartTLS + set_fact: + ansible_password: "{{ lookup('community.windows.laps_password', 'app01', + domain='dc01.ansible.com', + auth='simple', + start_tls=True, + username='username@ANSIBLE.COM', + password='SuperSecret123') }}" + +- name: Narrow down the search base to a an OU + set_fact: + ansible_password: "{{ lookup('community.windows.laps_password', 'sql10', + domain='dc01.ansible.com', + search_base='OU=Databases,DC=ansible,DC=com') }}" + +- name: Set certificate file to use when validating the TLS certificate + set_fact: + ansible_password: "{{ lookup('community.windows.laps_password', 'windows-pc', + domain='dc01.ansible.com', + start_tls=True, + ca_cert='/usr/local/share/certs/ad.pem') }}" +""" + +RETURN = """ +_raw: + description: + - The LAPS password(s) for the host(s) requested. + type: str +""" + +import os +import traceback + +from ansible.errors import AnsibleLookupError +from ansible.module_utils._text import to_bytes, to_native, to_text +from ansible.module_utils.basic import missing_required_lib +from ansible.plugins.lookup import LookupBase + +LDAP_IMP_ERR = None +try: + import ldap + import ldapurl + HAS_LDAP = True +except ImportError: + LDAP_IMP_ERR = traceback.format_exc() + HAS_LDAP = False + + +def get_laps_password(conn, cn, search_base): + search_filter = u"(&(objectClass=computer)(CN=%s))" % to_text(cn) + + ldap_results = conn.search_s(to_text(search_base), ldap.SCOPE_SUBTREE, search_filter, + attrlist=[u"distinguishedName", u"ms-Mcs-AdmPwd"]) + + # Filter out non server hosts, search_s seems to return 3 extra entries + # that are not computer classes, they do not have a distinguished name + # set in the returned results + valid_results = [attr for dn, attr in ldap_results if dn] + + if len(valid_results) == 0: + raise AnsibleLookupError("Failed to find the server '%s' in the base '%s'" % (cn, search_base)) + elif len(valid_results) > 1: + found_servers = [to_native(attr['distinguishedName'][0]) for attr in valid_results] + raise AnsibleLookupError("Found too many results for the server '%s' in the base '%s'. Specify a more " + "explicit search base for the server required. Found servers '%s'" + % (cn, search_base, "', '".join(found_servers))) + + password = valid_results[0].get('ms-Mcs-AdmPwd', None) + if not password: + distinguished_name = to_native(valid_results[0]['distinguishedName'][0]) + raise AnsibleLookupError("The server '%s' did not have the LAPS attribute 'ms-Mcs-AdmPwd'" % distinguished_name) + + return to_native(password[0]) + + +class LookupModule(LookupBase): + + def run(self, terms, variables=None, **kwargs): + if not HAS_LDAP: + msg = missing_required_lib("python-ldap", url="https://pypi.org/project/python-ldap/") + msg += ". Import Error: %s" % LDAP_IMP_ERR + raise AnsibleLookupError(msg) + + # Load the variables and direct args into the lookup options + self.set_options(var_options=variables, direct=kwargs) + domain = self.get_option('domain') + port = self.get_option('port') + scheme = self.get_option('scheme') + start_tls = self.get_option('start_tls') + validate_certs = self.get_option('validate_certs') + cacert_file = self.get_option('ca_cert') + search_base = self.get_option('search_base') + username = self.get_option('username') + password = self.get_option('password') + auth = self.get_option('auth') + allow_plaintext = self.get_option('allow_plaintext') + + # Validate and set input values + # https://www.openldap.org/lists/openldap-software/200202/msg00456.html + validate_certs_map = { + 'never': ldap.OPT_X_TLS_NEVER, + 'allow': ldap.OPT_X_TLS_ALLOW, + 'try': ldap.OPT_X_TLS_TRY, + 'demand': ldap.OPT_X_TLS_DEMAND, # Same as OPT_X_TLS_HARD + } + validate_certs_value = validate_certs_map.get(validate_certs, None) + if validate_certs_value is None: + valid_keys = list(validate_certs_map.keys()) + valid_keys.sort() + raise AnsibleLookupError("Invalid validate_certs value '%s': valid values are '%s'" + % (validate_certs, "', '".join(valid_keys))) + + if auth not in ['gssapi', 'simple']: + raise AnsibleLookupError("Invalid auth value '%s': expecting either 'gssapi', or 'simple'" % auth) + elif auth == 'gssapi': + if not ldap.SASL_AVAIL: + raise AnsibleLookupError("Cannot use auth=gssapi when SASL is not configured with the local LDAP " + "install") + if username or password: + raise AnsibleLookupError("Explicit credentials are not supported when auth='gssapi'. Call kinit " + "outside of Ansible") + elif auth == 'simple' and not (username and password): + raise AnsibleLookupError("The username and password values are required when auth=simple") + + if ldapurl.isLDAPUrl(domain): + ldap_url = ldapurl.LDAPUrl(ldapUrl=domain) + else: + port = port if port else 389 if scheme == 'ldap' else 636 + ldap_url = ldapurl.LDAPUrl(hostport="%s:%d" % (domain, port), urlscheme=scheme) + + # We have encryption if using LDAPS, or StartTLS is used, or we auth with SASL/GSSAPI + encrypted = ldap_url.urlscheme == 'ldaps' or start_tls or auth == 'gssapi' + if not encrypted and not allow_plaintext: + raise AnsibleLookupError("Current configuration will result in plaintext traffic exposing credentials. " + "Set auth=gssapi, scheme=ldaps, start_tls=True, or allow_plaintext=True to " + "continue") + + if ldap_url.urlscheme == 'ldaps' or start_tls: + # We cannot use conn.set_option as OPT_X_TLS_NEWCTX (required to use the new context) is not supported on + # older distros like EL7. Setting it on the ldap object works instead + if not ldap.TLS_AVAIL: + raise AnsibleLookupError("Cannot use TLS as the local LDAP installed has not been configured to support it") + + ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, validate_certs_value) + if cacert_file: + cacert_path = os.path.expanduser(os.path.expandvars(cacert_file)) + if not os.path.exists(to_bytes(cacert_path)): + raise AnsibleLookupError("The cacert_file specified '%s' does not exist" % to_native(cacert_path)) + + try: + # While this is a path, python-ldap expects a str/unicode and not bytes + ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, to_text(cacert_path)) + except ValueError: + # https://keathmilligan.net/python-ldap-and-macos/ + raise AnsibleLookupError("Failed to set path to cacert file, this is a known issue with older " + "OpenLDAP libraries on the host. Update OpenLDAP and reinstall " + "python-ldap to continue") + + conn_url = ldap_url.initializeUrl() + conn = ldap.initialize(conn_url, bytes_mode=False) + conn.set_option(ldap.OPT_PROTOCOL_VERSION, 3) + conn.set_option(ldap.OPT_REFERRALS, 0) # Allow us to search from the base + + # Make sure we run StartTLS before doing the bind to protect the credentials + if start_tls: + try: + conn.start_tls_s() + except ldap.LDAPError as err: + raise AnsibleLookupError("Failed to send StartTLS to LDAP host '%s': %s" + % (conn_url, to_native(err))) + + if auth == 'simple': + try: + conn.bind_s(to_text(username), to_text(password)) + except ldap.LDAPError as err: + raise AnsibleLookupError("Failed to simple bind against LDAP host '%s': %s" + % (conn_url, to_native(err))) + else: + try: + conn.sasl_gssapi_bind_s() + except ldap.AUTH_UNKNOWN as err: + # The SASL GSSAPI binding is not installed, e.g. cyrus-sasl-gssapi. Give a better error message than + # what python-ldap provides + raise AnsibleLookupError("Failed to do a sasl bind against LDAP host '%s', the GSSAPI mech is not " + "installed: %s" % (conn_url, to_native(err))) + except ldap.LDAPError as err: + raise AnsibleLookupError("Failed to do a sasl bind against LDAP host '%s': %s" + % (conn_url, to_native(err))) + + try: + if not search_base: + root_dse = conn.read_rootdse_s() + search_base = root_dse['defaultNamingContext'][0] + + ret = [] + # TODO: change method to search for all servers in 1 request instead of multiple requests + for server in terms: + ret.append(get_laps_password(conn, server, search_base)) + finally: + conn.unbind_s() + + return ret diff --git a/ansible_collections/community/windows/plugins/modules/__init__.py b/ansible_collections/community/windows/plugins/modules/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/__init__.py diff --git a/ansible_collections/community/windows/plugins/modules/psexec.py b/ansible_collections/community/windows/plugins/modules/psexec.py new file mode 100644 index 000000000..36541ca67 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/psexec.py @@ -0,0 +1,510 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Jordan Borean <jborean93@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: psexec +short_description: Runs commands on a remote Windows host based on the PsExec + model +description: +- Runs a remote command from a Linux host to a Windows host without WinRM being + set up. +- Can be run on the Ansible controller to bootstrap Windows hosts to get them + ready for WinRM. +options: + hostname: + description: + - The remote Windows host to connect to, can be either an IP address or a + hostname. + type: str + required: yes + connection_username: + description: + - The username to use when connecting to the remote Windows host. + - This user must be a member of the C(Administrators) group of the Windows + host. + - Required if the Kerberos requirements are not installed or the username + is a local account to the Windows host. + - Can be omitted to use the default Kerberos principal ticket in the + local credential cache if the Kerberos library is installed. + - If I(process_username) is not specified, then the remote process will run + under a Network Logon under this account. + type: str + connection_password: + description: + - The password for I(connection_user). + - Required if the Kerberos requirements are not installed or the username + is a local account to the Windows host. + - Can be omitted to use a Kerberos principal ticket for the principal set + by I(connection_user) if the Kerberos library is installed and the + ticket has already been retrieved with the C(kinit) command before. + type: str + port: + description: + - The port that the remote SMB service is listening on. + type: int + default: 445 + encrypt: + description: + - Will use SMB encryption to encrypt the SMB messages sent to and from the + host. + - This requires the SMB 3 protocol which is only supported from Windows + Server 2012 or Windows 8, older versions like Windows 7 or Windows Server + 2008 (R2) must set this to C(no) and use no encryption. + - When setting to C(no), the packets are in plaintext and can be seen by + anyone sniffing the network, any process options are included in this. + type: bool + default: yes + connection_timeout: + description: + - The timeout in seconds to wait when receiving the initial SMB negotiate + response from the server. + type: int + default: 60 + executable: + description: + - The executable to run on the Windows host. + type: str + required: yes + arguments: + description: + - Any arguments as a single string to use when running the executable. + type: str + working_directory: + description: + - Changes the working directory set when starting the process. + type: str + default: C:\Windows\System32 + asynchronous: + description: + - Will run the command as a detached process and the module returns + immediately after starting the process while the process continues to + run in the background. + - The I(stdout) and I(stderr) return values will be null when this is set + to C(yes). + - The I(stdin) option does not work with this type of process. + - The I(rc) return value is not set when this is C(yes) + type: bool + default: no + load_profile: + description: + - Runs the remote command with the user's profile loaded. + type: bool + default: yes + process_username: + description: + - The user to run the process as. + - This can be set to run the process under an Interactive logon of the + specified account which bypasses limitations of a Network logon used when + this isn't specified. + - If omitted then the process is run under the same account as + I(connection_username) with a Network logon. + - Set to C(System) to run as the builtin SYSTEM account, no password is + required with this account. + - If I(encrypt) is C(no), the username and password are sent as a simple + XOR scrambled byte string that is not encrypted. No special tools are + required to get the username and password just knowledge of the protocol. + type: str + process_password: + description: + - The password for I(process_username). + - Required if I(process_username) is defined and not C(System). + type: str + integrity_level: + description: + - The integrity level of the process when I(process_username) is defined + and is not equal to C(System). + - When C(default), the default integrity level based on the system setup. + - When C(elevated), the command will be run with Administrative rights. + - When C(limited), the command will be forced to run with + non-Administrative rights. + type: str + choices: + - limited + - default + - elevated + default: default + interactive: + description: + - Will run the process as an interactive process that shows a process + Window of the Windows session specified by I(interactive_session). + - The I(stdout) and I(stderr) return values will be null when this is set + to C(yes). + - The I(stdin) option does not work with this type of process. + type: bool + default: no + interactive_session: + description: + - The Windows session ID to use when displaying the interactive process on + the remote Windows host. + - This is only valid when I(interactive) is C(yes). + - The default is C(0) which is the console session of the Windows host. + type: int + default: 0 + priority: + description: + - Set the command's priority on the Windows host. + - See U(https://msdn.microsoft.com/en-us/library/windows/desktop/ms683211.aspx) + for more details. + type: str + choices: + - above_normal + - below_normal + - high + - idle + - normal + - realtime + default: normal + show_ui_on_logon_screen: + description: + - Shows the process UI on the Winlogon secure desktop when + I(process_username) is C(System). + type: bool + default: no + process_timeout: + description: + - The timeout in seconds that is placed upon the running process. + - A value of C(0) means no timeout. + type: int + default: 0 + stdin: + description: + - Data to send on the stdin pipe once the process has started. + - This option has no effect when I(interactive) or I(asynchronous) is + C(yes). + type: str +requirements: +- pypsexec +- smbprotocol[kerberos] for optional Kerberos authentication +notes: +- This module requires the Windows host to have SMB configured and enabled, + and port 445 opened on the firewall. +- This module will wait until the process is finished unless I(asynchronous) + is C(yes), ensure the process is run as a non-interactive command to avoid + infinite hangs waiting for input. +- The I(connection_username) must be a member of the local Administrator group + of the Windows host. For non-domain joined hosts, the + C(LocalAccountTokenFilterPolicy) should be set to C(1) to ensure this works, + see U(https://support.microsoft.com/en-us/help/951016/description-of-user-account-control-and-remote-restrictions-in-windows). +- For more information on this module and the various host requirements, see + U(https://github.com/jborean93/pypsexec). +seealso: +- module: ansible.builtin.raw +- module: ansible.windows.win_command +- module: community.windows.win_psexec +- module: ansible.windows.win_shell +author: +- Jordan Borean (@jborean93) +''' + +EXAMPLES = r''' +- name: Run a cmd.exe command + community.windows.psexec: + hostname: server + connection_username: username + connection_password: password + executable: cmd.exe + arguments: /c echo Hello World + +- name: Run a PowerShell command + community.windows.psexec: + hostname: server.domain.local + connection_username: username@DOMAIN.LOCAL + connection_password: password + executable: powershell.exe + arguments: Write-Host Hello World + +- name: Send data through stdin + community.windows.psexec: + hostname: 192.168.1.2 + connection_username: username + connection_password: password + executable: powershell.exe + arguments: '-' + stdin: | + Write-Host Hello World + Write-Error Error Message + exit 0 + +- name: Run the process as a different user + community.windows.psexec: + hostname: server + connection_user: username + connection_password: password + executable: whoami.exe + arguments: /all + process_username: anotheruser + process_password: anotherpassword + +- name: Run the process asynchronously + community.windows.psexec: + hostname: server + connection_username: username + connection_password: password + executable: cmd.exe + arguments: /c rmdir C:\temp + asynchronous: yes + +- name: Use Kerberos authentication for the connection (requires smbprotocol[kerberos]) + community.windows.psexec: + hostname: host.domain.local + connection_username: user@DOMAIN.LOCAL + executable: C:\some\path\to\executable.exe + arguments: /s + +- name: Disable encryption to work with WIndows 7/Server 2008 (R2) + community.windows.psexec: + hostanme: windows-pc + connection_username: Administrator + connection_password: Password01 + encrypt: no + integrity_level: elevated + process_username: Administrator + process_password: Password01 + executable: powershell.exe + arguments: (New-Object -ComObject Microsoft.Update.Session).CreateUpdateInstaller().IsBusy + +- name: Download and run ConfigureRemotingForAnsible.ps1 to setup WinRM + community.windows.psexec: + hostname: '{{ hostvars[inventory_hostname]["ansible_host"] | default(inventory_hostname) }}' + connection_username: '{{ ansible_user }}' + connection_password: '{{ ansible_password }}' + encrypt: yes + executable: powershell.exe + arguments: '-' + stdin: | + $ErrorActionPreference = "Stop" + $sec_protocols = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::SystemDefault + $sec_protocols = $sec_protocols -bor [Net.SecurityProtocolType]::Tls12 + [Net.ServicePointManager]::SecurityProtocol = $sec_protocols + $url = "https://github.com/ansible/ansible/raw/devel/examples/scripts/ConfigureRemotingForAnsible.ps1" + Invoke-Expression ((New-Object Net.WebClient).DownloadString($url)) + exit + delegate_to: localhost +''' + +RETURN = r''' +msg: + description: Any exception details when trying to run the process + returned: module failed + type: str + sample: 'Received exception from remote PAExec service: Failed to start "invalid.exe". The system cannot find the file specified. [Err=0x2, 2]' +stdout: + description: The stdout from the remote process + returned: success and interactive or asynchronous is 'no' + type: str + sample: Hello World +stderr: + description: The stderr from the remote process + returned: success and interactive or asynchronous is 'no' + type: str + sample: Error [10] running process +pid: + description: The process ID of the asynchronous process that was created + returned: success and asynchronous is 'yes' + type: int + sample: 719 +rc: + description: The return code of the remote process + returned: success and asynchronous is 'no' + type: int + sample: 0 +''' + +import traceback + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils._text import to_bytes, to_text + +PYPSEXEC_IMP_ERR = None +try: + from pypsexec import client + from pypsexec.exceptions import PypsexecException, PDUException, SCMRException + from pypsexec.paexec import ProcessPriority + from smbprotocol.exceptions import SMBException, SMBAuthenticationError, \ + SMBResponseException + import socket + HAS_PYPSEXEC = True +except ImportError: + PYPSEXEC_IMP_ERR = traceback.format_exc() + HAS_PYPSEXEC = False + +KERBEROS_IMP_ERR = None +try: + import gssapi # pylint: disable=unused-import + # GSSAPI extension required for Kerberos Auth in SMB + from gssapi.raw import inquire_sec_context_by_oid # pylint: disable=unused-import + HAS_KERBEROS = True +except ImportError: + KERBEROS_IMP_ERR = traceback.format_exc() + HAS_KERBEROS = False + + +def remove_artifacts(module, client): + try: + client.remove_service() + except (SMBException, PypsexecException) as exc: + module.warn("Failed to cleanup PAExec service and executable: %s" + % to_text(exc)) + + +def main(): + module_args = dict( + hostname=dict(type='str', required=True), + connection_username=dict(type='str'), + connection_password=dict(type='str', no_log=True), + port=dict(type='int', required=False, default=445), + encrypt=dict(type='bool', default=True), + connection_timeout=dict(type='int', default=60), + executable=dict(type='str', required=True), + arguments=dict(type='str'), + working_directory=dict(type='str', default=r'C:\Windows\System32'), + asynchronous=dict(type='bool', default=False), + load_profile=dict(type='bool', default=True), + process_username=dict(type='str'), + process_password=dict(type='str', no_log=True), + integrity_level=dict(type='str', default='default', + choices=['default', 'elevated', 'limited']), + interactive=dict(type='bool', default=False), + interactive_session=dict(type='int', default=0), + priority=dict(type='str', default='normal', + choices=['above_normal', 'below_normal', 'high', + 'idle', 'normal', 'realtime']), + show_ui_on_logon_screen=dict(type='bool', default=False), + process_timeout=dict(type='int', default=0), + stdin=dict(type='str') + ) + result = dict( + changed=False, + ) + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=False, + ) + + process_username = module.params['process_username'] + process_password = module.params['process_password'] + use_system = False + if process_username is not None and process_username.lower() == "system": + use_system = True + process_username = None + process_password = None + + if process_username is not None and process_password is None: + module.fail_json(msg='parameters are required together when not ' + 'running as System: process_username, ' + 'process_password') + if not HAS_PYPSEXEC: + module.fail_json(msg=missing_required_lib("pypsexec"), + exception=PYPSEXEC_IMP_ERR) + + hostname = module.params['hostname'] + connection_username = module.params['connection_username'] + connection_password = module.params['connection_password'] + port = module.params['port'] + encrypt = module.params['encrypt'] + connection_timeout = module.params['connection_timeout'] + executable = module.params['executable'] + arguments = module.params['arguments'] + working_directory = module.params['working_directory'] + asynchronous = module.params['asynchronous'] + load_profile = module.params['load_profile'] + elevated = module.params['integrity_level'] == "elevated" + limited = module.params['integrity_level'] == "limited" + interactive = module.params['interactive'] + interactive_session = module.params['interactive_session'] + + priority = { + "above_normal": ProcessPriority.ABOVE_NORMAL_PRIORITY_CLASS, + "below_normal": ProcessPriority.BELOW_NORMAL_PRIORITY_CLASS, + "high": ProcessPriority.HIGH_PRIORITY_CLASS, + "idle": ProcessPriority.IDLE_PRIORITY_CLASS, + "normal": ProcessPriority.NORMAL_PRIORITY_CLASS, + "realtime": ProcessPriority.REALTIME_PRIORITY_CLASS + }[module.params['priority']] + show_ui_on_logon_screen = module.params['show_ui_on_logon_screen'] + + process_timeout = module.params['process_timeout'] + stdin = module.params['stdin'] + + if (connection_username is None or connection_password is None) and \ + not HAS_KERBEROS: + module.fail_json(msg=missing_required_lib("gssapi"), + execption=KERBEROS_IMP_ERR) + + win_client = client.Client(server=hostname, username=connection_username, + password=connection_password, port=port, + encrypt=encrypt) + + try: + win_client.connect(timeout=connection_timeout) + except SMBAuthenticationError as exc: + module.fail_json(msg='Failed to authenticate over SMB: %s' + % to_text(exc)) + except SMBResponseException as exc: + module.fail_json(msg='Received unexpected SMB response when opening ' + 'the connection: %s' % to_text(exc)) + except PDUException as exc: + module.fail_json(msg='Received an exception with RPC PDU message: %s' + % to_text(exc)) + except SCMRException as exc: + module.fail_json(msg='Received an exception when dealing with SCMR on ' + 'the Windows host: %s' % to_text(exc)) + except (SMBException, PypsexecException) as exc: + module.fail_json(msg=to_text(exc)) + except socket.error as exc: + module.fail_json(msg=to_text(exc)) + + # create PAExec service and run the process + result['changed'] = True + b_stdin = to_bytes(stdin, encoding='utf-8') if stdin else None + run_args = dict( + executable=executable, arguments=arguments, asynchronous=asynchronous, + load_profile=load_profile, interactive=interactive, + interactive_session=interactive_session, + run_elevated=elevated, run_limited=limited, + username=process_username, password=process_password, + use_system_account=use_system, working_dir=working_directory, + priority=priority, show_ui_on_win_logon=show_ui_on_logon_screen, + timeout_seconds=process_timeout, stdin=b_stdin + ) + try: + win_client.create_service() + except (SMBException, PypsexecException) as exc: + module.fail_json(msg='Failed to create PAExec service: %s' + % to_text(exc)) + + try: + proc_result = win_client.run_executable(**run_args) + except (SMBException, PypsexecException) as exc: + module.fail_json(msg='Received error when running remote process: %s' + % to_text(exc)) + finally: + remove_artifacts(module, win_client) + + if asynchronous: + result['pid'] = proc_result[2] + elif interactive: + result['rc'] = proc_result[2] + else: + result['stdout'] = proc_result[0] + result['stderr'] = proc_result[1] + result['rc'] = proc_result[2] + + # close the SMB connection + try: + win_client.disconnect() + except (SMBException, PypsexecException) as exc: + module.warn("Failed to close the SMB connection: %s" % to_text(exc)) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/windows/plugins/modules/win_audit_policy_system.ps1 b/ansible_collections/community/windows/plugins/modules/win_audit_policy_system.ps1 new file mode 100644 index 000000000..07640ceee --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_audit_policy_system.ps1 @@ -0,0 +1,132 @@ +#!powershell + +# Copyright: (c) 2017, Noah Sparks <nsparks@outlook.com> +# 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 + +$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 + +$results = @{ + changed = $false +} + +###################################### +### populate sets for -validateset ### +###################################### +$categories_rc = run-command -command 'auditpol /list /category /r' +$subcategories_rc = run-command -command 'auditpol /list /subcategory:* /r' + +If ($categories_rc.item('rc') -eq 0) { + $categories = ConvertFrom-Csv $categories_rc.item('stdout') | Select-Object -expand Category* +} +Else { + Fail-Json -obj $results -message "Failed to retrive audit policy categories. Please make sure the auditpol command is functional on + the system and that the account ansible is running under is able to retrieve them. $($_.Exception.Message)" +} + +If ($subcategories_rc.item('rc') -eq 0) { + $subcategories = ConvertFrom-Csv $subcategories_rc.item('stdout') | Select-Object -expand Category* | + Where-Object { $_ -notin $categories } +} +Else { + Fail-Json -obj $results -message "Failed to retrive audit policy subcategories. Please make sure the auditpol command is functional on + the system and that the account ansible is running under is able to retrieve them. $($_.Exception.Message)" +} + +###################### +### ansible params ### +###################### +$category = Get-AnsibleParam -obj $params -name "category" -type "str" -ValidateSet $categories +$subcategory = Get-AnsibleParam -obj $params -name "subcategory" -type "str" -ValidateSet $subcategories +$audit_type = Get-AnsibleParam -obj $params -name "audit_type" -type "list" -failifempty - + +######################## +### Start Processing ### +######################## +Function Get-AuditPolicy ($GetString) { + $auditpolcsv = Run-Command -command $GetString + If ($auditpolcsv.item('rc') -eq 0) { + $Obj = ConvertFrom-CSV $auditpolcsv.item('stdout') | Select-Object @{n = 'subcategory'; e = { $_.Subcategory.ToLower() } }, + @{ n = 'audit_type'; e = { $_."Inclusion Setting".ToLower() } } + } + Else { + return $auditpolcsv.item('stderr') + } + + $HT = @{} + Foreach ( $Item in $Obj ) { + $HT.Add($Item.subcategory, $Item.audit_type) + } + $HT +} + +################ +### Validate ### +################ + +#make sure category and subcategory are valid +If (-Not $category -and -Not $subcategory) { Fail-Json -obj $results -message "You must provide either a Category or Subcategory parameter" } +If ($category -and $subcategory) { Fail-Json -obj $results -message "Must pick either a specific subcategory or category. You cannot define both" } + + +$possible_audit_types = 'success', 'failure', 'none' +$audit_type | ForEach-Object { + If ($_ -notin $possible_audit_types) { + Fail-Json -obj $result -message "$_ is not a valid audit_type. Please choose from $($possible_audit_types -join ',')" + } +} + +############################################################# +### build lists for setting, getting, and comparing rules ### +############################################################# +$audit_type_string = $audit_type -join ' and ' + +$SetString = 'auditpol /set' +$GetString = 'auditpol /get /r' + +If ($category) { $SetString = "$SetString /category:`"$category`""; $GetString = "$GetString /category:`"$category`"" } +If ($subcategory) { $SetString = "$SetString /subcategory:`"$subcategory`""; $GetString = "$GetString /subcategory:`"$subcategory`"" } + + +Switch ($audit_type_string) { + 'success and failure' { $SetString = "$SetString /success:enable /failure:enable"; $audit_type_check = $audit_type_string } + 'failure' { $SetString = "$SetString /success:disable /failure:enable"; $audit_type_check = $audit_type_string } + 'success' { $SetString = "$SetString /success:enable /failure:disable"; $audit_type_check = $audit_type_string } + 'none' { $SetString = "$SetString /success:disable /failure:disable"; $audit_type_check = 'No Auditing' } + default { Fail-Json -obj $result -message "It seems you have specified an invalid combination of items for audit_type. Please review documentation" } +} + +######################### +### check Idempotence ### +######################### + +$CurrentRule = Get-AuditPolicy $GetString + +#exit if the audit_type is already set properly for the category +If (-not ($CurrentRule.Values | Where-Object { $_ -ne $audit_type_check }) ) { + $results.current_audit_policy = Get-AuditPolicy $GetString + Exit-Json -obj $results +} + +#################### +### Apply Change ### +#################### + +If (-not $check_mode) { + $ApplyPolicy = Run-Command -command $SetString + + If ($ApplyPolicy.Item('rc') -ne 0) { + $results.current_audit_policy = Get-AuditPolicy $GetString + Fail-Json $results "Failed to set audit policy - $($_.Exception.Message)" + } +} + +$results.changed = $true +$results.current_audit_policy = Get-AuditPolicy $GetString +Exit-Json $results diff --git a/ansible_collections/community/windows/plugins/modules/win_audit_policy_system.py b/ansible_collections/community/windows/plugins/modules/win_audit_policy_system.py new file mode 100644 index 000000000..18a91e7b1 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_audit_policy_system.py @@ -0,0 +1,69 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2017, Noah Sparks <nsparks@outlook.com> +# Copyright: (c) 2017, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_audit_policy_system +short_description: Used to make changes to the system wide Audit Policy +description: + - Used to make changes to the system wide Audit Policy. +options: + category: + description: + - Single string value for the category you would like to adjust the policy on. + - Cannot be used with I(subcategory). You must define one or the other. + - Changing this setting causes all subcategories to be adjusted to the defined I(audit_type). + type: str + subcategory: + description: + - Single string value for the subcategory you would like to adjust the policy on. + - Cannot be used with I(category). You must define one or the other. + type: str + audit_type: + description: + - The type of event you would like to audit for. + - Accepts a list. See examples. + type: list + elements: str + required: yes + choices: [ failure, none, success ] +notes: + - It is recommended to take a backup of the policies before adjusting them for the first time. + - See this page for in depth information U(https://technet.microsoft.com/en-us/library/cc766468.aspx). +seealso: +- module: community.windows.win_audit_rule +author: + - Noah Sparks (@nwsparks) +''' + +EXAMPLES = r''' +- name: Enable failure auditing for the subcategory "File System" + community.windows.win_audit_policy_system: + subcategory: File System + audit_type: failure + +- name: Enable all auditing types for the category "Account logon events" + community.windows.win_audit_policy_system: + category: Account logon events + audit_type: success, failure + +- name: Disable auditing for the subcategory "File System" + community.windows.win_audit_policy_system: + subcategory: File System + audit_type: none +''' + +RETURN = r''' +current_audit_policy: + description: details on the policy being targetted + returned: always + type: dict + sample: |- + { + "File Share":"failure" + } +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_audit_rule.ps1 b/ansible_collections/community/windows/plugins/modules/win_audit_rule.ps1 new file mode 100644 index 000000000..c36c8f9cf --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_audit_rule.ps1 @@ -0,0 +1,174 @@ +#!powershell + +# Copyright: (c) 2017, Noah Sparks <nsparks@outlook.com> +# 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 + +$params = Parse-Args -arguments $args -supports_check_mode $true +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false + +# module parameters +$path = Get-AnsibleParam -obj $params -name "path" -type "path" -failifempty $true -aliases "destination", "dest" +$user = Get-AnsibleParam -obj $params -name "user" -type "str" -failifempty $true +$rights = Get-AnsibleParam -obj $params -name "rights" -type "list" +$inheritance_flags = Get-AnsibleParam -obj $params -name "inheritance_flags" -type "list" -default 'ContainerInherit', 'ObjectInherit' +$prop_options = 'InheritOnly', 'None', 'NoPropagateInherit' +$propagation_flags = Get-AnsibleParam -obj $params -name "propagation_flags" -type "str" -default "none" -ValidateSet $prop_options +$audit_flags = Get-AnsibleParam -obj $params -name "audit_flags" -type "list" -default 'success' +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset 'present', 'absent' + +#Make sure target path is valid +If (-not (Test-Path -Path $path) ) { + Fail-Json -obj $result -message "defined path ($path) is not found/invalid" +} + +#function get current audit rules and convert to hashtable +Function Get-CurrentAuditRule ($path) { + Try { + $ACL = Get-Acl $path -Audit + } + Catch { + Return "Unable to retrieve the ACL on $Path" + } + + $HT = Foreach ($Obj in $ACL.Audit) { + @{ + user = $Obj.IdentityReference.ToString() + rights = ($Obj | Select-Object -expand "*rights").ToString() + audit_flags = $Obj.AuditFlags.ToString() + is_inherited = $Obj.IsInherited.ToString() + inheritance_flags = $Obj.InheritanceFlags.ToString() + propagation_flags = $Obj.PropagationFlags.ToString() + } + } + + If (-Not $HT) { + "No audit rules defined on $path" + } + Else { $HT } +} + +$result = @{ + changed = $false + current_audit_rules = Get-CurrentAuditRule $path +} + +#Make sure identity is valid and can be looked up +Try { + $SID = Convert-ToSid $user +} +Catch { + Fail-Json -obj $result -message "Failed to lookup the identity ($user) - $($_.exception.message)" +} + +#get the path type +$ItemType = (Get-Item $path -Force).GetType() +switch ($ItemType) { + ([Microsoft.Win32.RegistryKey]) { $registry = $true; $result.path_type = 'registry' } + ([System.IO.FileInfo]) { $file = $true; $result.path_type = 'file' } + ([System.IO.DirectoryInfo]) { $result.path_type = 'directory' } +} + +#Get current acl/audit rules on the target +Try { + $ACL = Get-Acl $path -Audit +} +Catch { + Fail-Json -obj $result -message "Unable to retrieve the ACL on $Path - $($_.Exception.Message)" +} + +#configure acl object to remove the specified user +If ($state -eq 'absent') { + #Try and find an identity on the object that matches user + #We skip inherited items since we can't remove those + $ToRemove = ($ACL.Audit | Where-Object { $_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) -eq $SID -and + $_.IsInherited -eq $false }).IdentityReference + + #Exit with changed false if no identity is found + If (-Not $ToRemove) { + $result.current_audit_rules = Get-CurrentAuditRule $path + Exit-Json -obj $result + } + + #update the ACL object if identity found + Try { + $ToRemove | ForEach-Object { $ACL.PurgeAuditRules($_) } + } + Catch { + $result.current_audit_rules = Get-CurrentAuditRule $path + Fail-Json -obj $result -message "Failed to remove audit rule: $($_.Exception.Message)" + } +} + +Else { + If ($registry) { + $PossibleRights = [System.Enum]::GetNames([System.Security.AccessControl.RegistryRights]) + + Foreach ($right in $rights) { + if ($right -notin $PossibleRights) { + Fail-Json -obj $result -message "$right does not seem to be a valid REGISTRY right" + } + } + + $NewAccessRule = New-Object System.Security.AccessControl.RegistryAuditRule($user, $rights, $inheritance_flags, $propagation_flags, $audit_flags) + } + Else { + $PossibleRights = [System.Enum]::GetNames([System.Security.AccessControl.FileSystemRights]) + + Foreach ($right in $rights) { + if ($right -notin $PossibleRights) { + Fail-Json -obj $result -message "$right does not seem to be a valid FILE SYSTEM right" + } + } + + If ($file -and $inheritance_flags -ne 'none') { + Fail-Json -obj $result -message "The target type is a file. inheritance_flags must be changed to 'none'" + } + + $NewAccessRule = New-Object System.Security.AccessControl.FileSystemAuditRule($user, $rights, $inheritance_flags, $propagation_flags, $audit_flags) + } + + #exit here if any existing rule matches defined rule since no change is needed + #if we need to ignore inherited rules in the future, this would be where to do it + #Just filter out inherited rules from $ACL.Audit + Foreach ($group in $ACL.Audit | Where-Object { $_.IsInherited -eq $false }) { + If ( + ($group | Select-Object -expand "*Rights") -eq ($NewAccessRule | Select-Object -expand "*Rights") -and + $group.AuditFlags -eq $NewAccessRule.AuditFlags -and + $group.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) -eq $SID -and + $group.InheritanceFlags -eq $NewAccessRule.InheritanceFlags -and + $group.PropagationFlags -eq $NewAccessRule.PropagationFlags + ) { + $result.current_audit_rules = Get-CurrentAuditRule $path + Exit-Json -obj $result + } + } + + #try and set the acl object. AddAuditRule allows for multiple entries to exist under the same + #identity...so if someone wanted success: write and failure: delete for example, that setup would be + #possible. The alternative is SetAuditRule which would instead modify an existing rule and not allow + #for setting the above example. + Try { + $ACL.AddAuditRule($NewAccessRule) + } + Catch { + Fail-Json -obj $result -message "Failed to set the audit rule: $($_.Exception.Message)" + } +} + + +#finally set the permissions +Try { + Set-Acl -Path $path -ACLObject $ACL -WhatIf:$check_mode +} +Catch { + $result.current_audit_rules = Get-CurrentAuditRule $path + Fail-Json -obj $result -message "Failed to apply audit change: $($_.Exception.Message)" +} + +#exit here after a change is applied +$result.current_audit_rules = Get-CurrentAuditRule $path +$result.changed = $true +Exit-Json -obj $result diff --git a/ansible_collections/community/windows/plugins/modules/win_audit_rule.py b/ansible_collections/community/windows/plugins/modules/win_audit_rule.py new file mode 100644 index 000000000..fa9bd18a3 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_audit_rule.py @@ -0,0 +1,140 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2017, Noah Sparks <nsparks@outlook.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_audit_rule +short_description: Adds an audit rule to files, folders, or registry keys +description: + - Used to apply audit rules to files, folders or registry keys. + - Once applied, it will begin recording the user who performed the operation defined into the Security + Log in the Event viewer. + - The behavior is designed to ignore inherited rules since those cannot be adjusted without first disabling + the inheritance behavior. It will still print inherited rules in the output though for debugging purposes. +options: + path: + description: + - Path to the file, folder, or registry key. + - Registry paths should be in Powershell format, beginning with an abbreviation for the root + such as, C(HKLM:\Software). + type: path + required: yes + aliases: [ dest, destination ] + user: + description: + - The user or group to adjust rules for. + type: str + required: yes + rights: + description: + - Comma separated list of the rights desired. Only required for adding a rule. + - If I(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 I(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: list + elements: str + required: yes + inheritance_flags: + description: + - Defines what objects inside of a folder or registry key will inherit the settings. + - If you are setting a rule on a file, this value has to be changed to C(none). + - For more information on the choices see MSDN PropagationFlags enumeration + at U(https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.inheritanceflags.aspx). + type: list + elements: str + choices: [ ContainerInherit, ObjectInherit ] + default: ContainerInherit,ObjectInherit + propagation_flags: + description: + - Propagation flag on the audit rules. + - This value is ignored when the path type is a file. + - For more information on the choices see MSDN PropagationFlags enumeration + at U(https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.propagationflags.aspx). + choices: [ None, InherityOnly, NoPropagateInherit ] + default: "None" + audit_flags: + description: + - Defines whether to log on failure, success, or both. + - To log both define as comma separated list "Success, Failure". + type: list + elements: str + required: yes + choices: [ Failure, Success ] + state: + description: + - Whether the rule should be C(present) or C(absent). + - For absent, only I(path), I(user), and I(state) are required. + - Specifying C(absent) will remove all rules matching the defined I(user). + type: str + choices: [ absent, present ] + default: present +seealso: +- module: community.windows.win_audit_policy_system +author: + - Noah Sparks (@nwsparks) +''' + +EXAMPLES = r''' +- name: Add filesystem audit rule for a folder + community.windows.win_audit_rule: + path: C:\inetpub\wwwroot\website + user: BUILTIN\Users + rights: write,delete,changepermissions + audit_flags: success,failure + inheritance_flags: ContainerInherit,ObjectInherit + +- name: Add filesystem audit rule for a file + community.windows.win_audit_rule: + path: C:\inetpub\wwwroot\website\web.config + user: BUILTIN\Users + rights: write,delete,changepermissions + audit_flags: success,failure + inheritance_flags: None + +- name: Add registry audit rule + community.windows.win_audit_rule: + path: HKLM:\software + user: BUILTIN\Users + rights: delete + audit_flags: 'success' + +- name: Remove filesystem audit rule + community.windows.win_audit_rule: + path: C:\inetpub\wwwroot\website + user: BUILTIN\Users + state: absent + +- name: Remove registry audit rule + community.windows.win_audit_rule: + path: HKLM:\software + user: BUILTIN\Users + state: absent +''' + +RETURN = r''' +current_audit_rules: + description: + - The current rules on the defined I(path) + - Will return "No audit rules defined on I(path)" + returned: always + type: dict + sample: | + { + "audit_flags": "Success", + "user": "Everyone", + "inheritance_flags": "False", + "is_inherited": "False", + "propagation_flags": "None", + "rights": "Delete" + } +path_type: + description: + - The type of I(path) being targetted. + - Will be one of file, directory, registry. + returned: always + type: str +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_auto_logon.ps1 b/ansible_collections/community/windows/plugins/modules/win_auto_logon.ps1 new file mode 100644 index 000000000..66a20dd9e --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_auto_logon.ps1 @@ -0,0 +1,404 @@ +#!powershell + +# Copyright: (c) 2019, Prasoon Karunan V (@prasoonkarunan) <kvprasoon@Live.in> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# All helper methods are written in a binary module and has to be loaded for consuming them. +#AnsibleRequires -CSharpUtil Ansible.Basic +#Requires -Module Ansible.ModuleUtils.AddType + +Set-StrictMode -Version 2.0 + +$spec = @{ + options = @{ + logon_count = @{ type = "int" } + password = @{ type = "str"; no_log = $true } + state = @{ type = "str"; choices = "absent", "present"; default = "present" } + username = @{ type = "str" } + } + required_if = @( + , @("state", "present", @("username", "password")) + ) + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$logonCount = $module.Params.logon_count +$password = $module.Params.password +$state = $module.Params.state +$username = $module.Params.username +$domain = $null + +if ($username) { + # Try and get the Netlogon form of the username specified. Translating to and from a SID gives us an NTAccount + # in the Netlogon form that we desire. + $ntAccount = New-Object -TypeName System.Security.Principal.NTAccount -ArgumentList $username + try { + $accountSid = $ntAccount.Translate([System.Security.Principal.SecurityIdentifier]) + } + catch [System.Security.Principal.IdentityNotMappedException] { + $module.FailJson("Failed to find a local or domain user with the name '$username'", $_) + } + $ntAccount = $accountSid.Translate([System.Security.Principal.NTAccount]) + + $domain, $username = $ntAccount.Value -split '\\' +} + +# Make sure $null regardless of any input value if state: absent +if ($state -eq 'absent') { + $password = $null +} + +Add-CSharpType -AnsibleModule $module -References @' +using Microsoft.Win32.SafeHandles; +using System; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ansible.WinAutoLogon +{ + internal class NativeHelpers + { + [StructLayout(LayoutKind.Sequential)] + public class LSA_OBJECT_ATTRIBUTES + { + public UInt32 Length = 0; + public IntPtr RootDirectory = IntPtr.Zero; + public IntPtr ObjectName = IntPtr.Zero; + public UInt32 Attributes = 0; + public IntPtr SecurityDescriptor = IntPtr.Zero; + public IntPtr SecurityQualityOfService = IntPtr.Zero; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct LSA_UNICODE_STRING + { + public UInt16 Length; + public UInt16 MaximumLength; + public IntPtr Buffer; + + public static explicit operator string(LSA_UNICODE_STRING s) + { + byte[] strBytes = new byte[s.Length]; + Marshal.Copy(s.Buffer, strBytes, 0, s.Length); + return Encoding.Unicode.GetString(strBytes); + } + + public static SafeMemoryBuffer CreateSafeBuffer(string s) + { + if (s == null) + return new SafeMemoryBuffer(IntPtr.Zero); + + byte[] stringBytes = Encoding.Unicode.GetBytes(s); + int structSize = Marshal.SizeOf(typeof(LSA_UNICODE_STRING)); + IntPtr buffer = Marshal.AllocHGlobal(structSize + stringBytes.Length); + try + { + LSA_UNICODE_STRING lsaString = new LSA_UNICODE_STRING() + { + Length = (UInt16)(stringBytes.Length), + MaximumLength = (UInt16)(stringBytes.Length), + Buffer = IntPtr.Add(buffer, structSize), + }; + Marshal.StructureToPtr(lsaString, buffer, false); + Marshal.Copy(stringBytes, 0, lsaString.Buffer, stringBytes.Length); + return new SafeMemoryBuffer(buffer); + } + catch + { + // Make sure we free the pointer before raising the exception. + Marshal.FreeHGlobal(buffer); + throw; + } + } + } + } + + internal class NativeMethods + { + [DllImport("Advapi32.dll")] + public static extern UInt32 LsaClose( + IntPtr ObjectHandle); + + [DllImport("Advapi32.dll")] + public static extern UInt32 LsaFreeMemory( + IntPtr Buffer); + + [DllImport("Advapi32.dll")] + internal static extern Int32 LsaNtStatusToWinError( + UInt32 Status); + + [DllImport("Advapi32.dll")] + public static extern UInt32 LsaOpenPolicy( + IntPtr SystemName, + NativeHelpers.LSA_OBJECT_ATTRIBUTES ObjectAttributes, + LsaPolicyAccessMask AccessMask, + out SafeLsaHandle PolicyHandle); + + [DllImport("Advapi32.dll")] + public static extern UInt32 LsaRetrievePrivateData( + SafeLsaHandle PolicyHandle, + SafeMemoryBuffer KeyName, + out SafeLsaMemory PrivateData); + + [DllImport("Advapi32.dll")] + public static extern UInt32 LsaStorePrivateData( + SafeLsaHandle PolicyHandle, + SafeMemoryBuffer KeyName, + SafeMemoryBuffer PrivateData); + } + + internal class SafeLsaMemory : SafeBuffer + { + internal SafeLsaMemory() : base(true) { } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + + protected override bool ReleaseHandle() + { + return NativeMethods.LsaFreeMemory(handle) == 0; + } + } + + internal class SafeMemoryBuffer : SafeBuffer + { + internal SafeMemoryBuffer() : base(true) { } + + internal SafeMemoryBuffer(IntPtr ptr) : base(true) + { + base.SetHandle(ptr); + } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + + protected override bool ReleaseHandle() + { + if (handle != IntPtr.Zero) + Marshal.FreeHGlobal(handle); + return true; + } + } + + public class SafeLsaHandle : SafeHandleZeroOrMinusOneIsInvalid + { + internal SafeLsaHandle() : base(true) { } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + + protected override bool ReleaseHandle() + { + return NativeMethods.LsaClose(handle) == 0; + } + } + + public class Win32Exception : System.ComponentModel.Win32Exception + { + private string _exception_msg; + public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { } + public Win32Exception(int errorCode, string message) : base(errorCode) + { + _exception_msg = String.Format("{0} - {1} (Win32 Error Code {2}: 0x{3})", message, base.Message, errorCode, errorCode.ToString("X8")); + } + public override string Message { get { return _exception_msg; } } + public static explicit operator Win32Exception(string message) { return new Win32Exception(message); } + } + + [Flags] + public enum LsaPolicyAccessMask : uint + { + ViewLocalInformation = 0x00000001, + ViewAuditInformation = 0x00000002, + GetPrivateInformation = 0x00000004, + TrustAdmin = 0x00000008, + CreateAccount = 0x00000010, + CreateSecret = 0x00000020, + CreatePrivilege = 0x00000040, + SetDefaultQuotaLimits = 0x00000080, + SetAuditRequirements = 0x00000100, + AuditLogAdmin = 0x00000200, + ServerAdmin = 0x00000400, + LookupNames = 0x00000800, + Read = 0x00020006, + Write = 0x000207F8, + Execute = 0x00020801, + AllAccess = 0x000F0FFF, + } + + public class LsaUtil + { + public static SafeLsaHandle OpenPolicy(LsaPolicyAccessMask access) + { + NativeHelpers.LSA_OBJECT_ATTRIBUTES oa = new NativeHelpers.LSA_OBJECT_ATTRIBUTES(); + SafeLsaHandle lsaHandle; + UInt32 res = NativeMethods.LsaOpenPolicy(IntPtr.Zero, oa, access, out lsaHandle); + if (res != 0) + throw new Win32Exception(NativeMethods.LsaNtStatusToWinError(res), + String.Format("LsaOpenPolicy({0}) failed", access.ToString())); + return lsaHandle; + } + + public static string RetrievePrivateData(SafeLsaHandle handle, string key) + { + using (SafeMemoryBuffer keyBuffer = NativeHelpers.LSA_UNICODE_STRING.CreateSafeBuffer(key)) + { + SafeLsaMemory buffer; + UInt32 res = NativeMethods.LsaRetrievePrivateData(handle, keyBuffer, out buffer); + using (buffer) + { + if (res != 0) + { + // If the data object was not found we return null to indicate it isn't set. + if (res == 0xC0000034) // STATUS_OBJECT_NAME_NOT_FOUND + return null; + + throw new Win32Exception(NativeMethods.LsaNtStatusToWinError(res), + String.Format("LsaRetrievePrivateData({0}) failed", key)); + } + + NativeHelpers.LSA_UNICODE_STRING lsaString = (NativeHelpers.LSA_UNICODE_STRING) + Marshal.PtrToStructure(buffer.DangerousGetHandle(), + typeof(NativeHelpers.LSA_UNICODE_STRING)); + return (string)lsaString; + } + } + } + + public static void StorePrivateData(SafeLsaHandle handle, string key, string data) + { + using (SafeMemoryBuffer keyBuffer = NativeHelpers.LSA_UNICODE_STRING.CreateSafeBuffer(key)) + using (SafeMemoryBuffer dataBuffer = NativeHelpers.LSA_UNICODE_STRING.CreateSafeBuffer(data)) + { + UInt32 res = NativeMethods.LsaStorePrivateData(handle, keyBuffer, dataBuffer); + if (res != 0) + { + // When clearing the private data with null it may return this error which we can ignore. + if (data == null && res == 0xC0000034) // STATUS_OBJECT_NAME_NOT_FOUND + return; + + throw new Win32Exception(NativeMethods.LsaNtStatusToWinError(res), + String.Format("LsaStorePrivateData({0}) failed", key)); + } + } + } + } +} +'@ + +$autoLogonRegPath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' +$logonDetails = Get-ItemProperty -LiteralPath $autoLogonRegPath + +$before = @{ + state = 'absent' +} +if ('AutoAdminLogon' -in $logonDetails.PSObject.Properties.Name -and $logonDetails.AutoAdminLogon -eq 1) { + $before.state = 'present' +} + +$mapping = @{ + DefaultUserName = 'username' + DefaultDomainName = 'domain' + AutoLogonCount = 'logon_count' +} +foreach ($map_detail in $mapping.GetEnumerator()) { + if ($map_detail.Key -in $logonDetails.PSObject.Properties.Name) { + $before."$($map_detail.Value)" = $logonDetails."$($map_detail.Key)" + } +} + +$module.Diff.before = $before + +$propParams = @{ + LiteralPath = $autoLogonRegPath + WhatIf = $module.CheckMode + Force = $true +} + +# First set the registry information +# The DefaultPassword reg key should never be set, we use LSA to store the password in a more secure way. +if ('DefaultPassword' -in (Get-Item -LiteralPath $autoLogonRegPath).Property) { + # Bug on older Windows hosts where -WhatIf causes it fail to find the property + if (-not $module.CheckMode) { + Remove-ItemProperty -Name 'DefaultPassword' @propParams + } + $module.Result.changed = $true +} + +$autoLogonKeyList = @{ + DefaultUserName = @{ + before = if ($before.ContainsKey('username')) { $before.username } else { $null } + after = $username + } + DefaultDomainName = @{ + before = if ($before.ContainsKey('domain')) { $before.domain } else { $null } + after = $domain + } + AutoLogonCount = @{ + before = if ($before.ContainsKey('logon_count')) { $before.logon_count } else { $null } + after = $logonCount + } +} + +# Check AutoAdminLogon separately as it has different logic (key must exist) +if ($state -ne $before.state) { + $newValue = if ($state -eq 'present') { 1 } else { 0 } + $null = New-ItemProperty -Name 'AutoAdminLogon' -Value $newValue -PropertyType DWord @propParams + $module.Result.changed = $true +} + +foreach ($key in $autoLogonKeyList.GetEnumerator()) { + $beforeVal = $key.Value.before + $after = $key.Value.after + + if ($state -eq 'present' -and $beforeVal -cne $after) { + if ($null -ne $after) { + $null = New-ItemProperty -Name $key.Key -Value $after @propParams + } + elseif (-not $module.CheckMode) { + Remove-ItemProperty -Name $key.Key @propParams + } + $module.Result.changed = $true + } + elseif ($state -eq 'absent' -and $null -ne $beforeVal) { + if (-not $module.CheckMode) { + Remove-ItemProperty -Name $key.Key @propParams + } + $module.Result.changed = $true + } +} + +# Finally update the password in the LSA private store. +$lsaHandle = [Ansible.WinAutoLogon.LsaUtil]::OpenPolicy('CreateSecret, GetPrivateInformation') +try { + $beforePass = [Ansible.WinAutoLogon.LsaUtil]::RetrievePrivateData($lsaHandle, 'DefaultPassword') + + if ($beforePass -cne $password) { + # Due to .NET marshaling we need to pass in $null as NullString.Value so it's truly a null value. + if ($null -eq $password) { + $password = [NullString]::Value + } + if (-not $module.CheckMode) { + [Ansible.WinAutoLogon.LsaUtil]::StorePrivateData($lsaHandle, 'DefaultPassword', $password) + } + $module.Result.changed = $true + } +} +finally { + $lsaHandle.Dispose() +} + +# Need to manually craft the after diff in case we are running in check mode +$module.Diff.after = @{ + state = $state +} +if ($state -eq 'present') { + $module.Diff.after.username = $username + $module.Diff.after.domain = $domain + if ($null -ne $logonCount) { + $module.Diff.after.logon_count = $logonCount + } +} + +$module.ExitJson() + diff --git a/ansible_collections/community/windows/plugins/modules/win_auto_logon.py b/ansible_collections/community/windows/plugins/modules/win_auto_logon.py new file mode 100644 index 000000000..127725fba --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_auto_logon.py @@ -0,0 +1,72 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Prasoon Karunan V (@prasoonkarunan) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_auto_logon +short_description: Adds or Sets auto logon registry keys. +description: + - Used to apply auto logon registry setting. +options: + logon_count: + description: + - The number of times to do an automatic logon. + - This count is deremented by Windows everytime an automatic logon is + performed. + - Once the count reaches C(0) then the automatic logon process is + disabled. + type: int + username: + description: + - Username to login automatically. + - Must be set when C(state=present). + - This can be the Netlogon or UPN of a domain account and is + automatically parsed to the C(DefaultUserName) and C(DefaultDomainName) + registry properties. + type: str + password: + description: + - Password to be used for automatic login. + - Must be set when C(state=present). + - Value of this input will be used as password for I(username). + - While this value is encrypted by LSA it is decryptable to any user who + is an Administrator on the remote host. + type: str + state: + description: + - Whether the registry key should be C(present) or C(absent). + type: str + choices: [ absent, present ] + default: present +author: + - Prasoon Karunan V (@prasoonkarunan) +''' + +EXAMPLES = r''' +- name: Set autologon for user1 + community.windows.win_auto_logon: + username: User1 + password: str0ngp@ssword + +- name: Set autologon for abc.com\user1 + community.windows.win_auto_logon: + username: abc.com\User1 + password: str0ngp@ssword + +- name: Remove autologon for user1 + community.windows.win_auto_logon: + state: absent + +- name: Set autologon for user1 with a limited logon count + community.windows.win_auto_logon: + username: User1 + password: str0ngp@ssword + logon_count: 5 +''' + +RETURN = r''' +# +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_certificate_info.ps1 b/ansible_collections/community/windows/plugins/modules/win_certificate_info.ps1 new file mode 100644 index 000000000..004cd3e34 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_certificate_info.ps1 @@ -0,0 +1,115 @@ +#!powershell + +# Copyright: (c) 2019, Micah Hunsberger +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic + +function ConvertTo-Timestamp($start_date, $end_date) { + if ($start_date -and $end_date) { + return (New-TimeSpan -Start $start_date -End $end_date).TotalSeconds + } +} + +function Format-Date([DateTime]$date) { + return $date.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssK') +} + +function Get-CertificateInfo ($cert) { + $epoch_date = Get-Date -Date "01/01/1970" + + $cert_info = @{ extensions = @() } + $cert_info.friendly_name = $cert.FriendlyName + $cert_info.thumbprint = $cert.Thumbprint + $cert_info.subject = $cert.Subject + $cert_info.issuer = $cert.Issuer + $cert_info.valid_from = (ConvertTo-Timestamp -start_date $epoch_date -end_date $cert.NotBefore.ToUniversalTime()) + $cert_info.valid_from_iso8601 = Format-Date -date $cert.NotBefore + $cert_info.valid_to = (ConvertTo-Timestamp -start_date $epoch_date -end_date $cert.NotAfter.ToUniversalTime()) + $cert_info.valid_to_iso8601 = Format-Date -date $cert.NotAfter + $cert_info.serial_number = $cert.SerialNumber + $cert_info.archived = $cert.Archived + $cert_info.version = $cert.Version + $cert_info.has_private_key = $cert.HasPrivateKey + $cert_info.issued_by = $cert.GetNameInfo('SimpleName', $true) + $cert_info.issued_to = $cert.GetNameInfo('SimpleName', $false) + $cert_info.signature_algorithm = $cert.SignatureAlgorithm.FriendlyName + $cert_info.dns_names = [System.Collections.Generic.List`1[String]]@($cert_info.issued_to) + $cert_info.raw = [System.Convert]::ToBase64String($cert.GetRawCertData()) + $cert_info.public_key = [System.Convert]::ToBase64String($cert.GetPublicKey()) + if ($cert.Extensions.Count -gt 0) { + [array]$cert_info.extensions = foreach ($extension in $cert.Extensions) { + $extension_info = @{ + critical = $extension.Critical + field = $extension.Oid.FriendlyName + value = $extension.Format($false) + } + if ($extension -is [System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension]) { + $cert_info.is_ca = $extension.CertificateAuthority + $cert_info.path_length_constraint = $extension.PathLengthConstraint + } + elseif ($extension -is [System.Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension]) { + $cert_info.intended_purposes = $extension.EnhancedKeyUsages.FriendlyName -as [string[]] + } + elseif ($extension -is [System.Security.Cryptography.X509Certificates.X509KeyUsageExtension]) { + $cert_info.key_usages = $extension.KeyUsages.ToString().Split(',').Trim() -as [string[]] + } + elseif ($extension -is [System.Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierExtension]) { + $cert_info.ski = $extension.SubjectKeyIdentifier + } + elseif ($extension.Oid.value -eq '2.5.29.17') { + $sans = $extension.Format($true).Split("`r`n", [System.StringSplitOptions]::RemoveEmptyEntries) + foreach ($san in $sans) { + $san_parts = $san.Split("=") + if ($san_parts.Length -ge 2 -and $san_parts[0].Trim() -eq 'DNS Name') { + $cert_info.dns_names.Add($san_parts[1].Trim()) + } + } + } + $extension_info + } + } + return $cert_info +} + +$store_location_values = ([System.Security.Cryptography.X509Certificates.StoreLocation]).GetEnumValues() | ForEach-Object { $_.ToString() } + +$spec = @{ + options = @{ + thumbprint = @{ type = "str"; required = $false } + store_name = @{ type = "str"; default = "My"; } + store_location = @{ type = "str"; default = "LocalMachine"; choices = $store_location_values; } + } + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$thumbprint = $module.Params.thumbprint +$store_name = $module.Params.store_name +$store_location = [System.Security.Cryptography.X509Certificates.Storelocation]"$($module.Params.store_location)" + +$store = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $store_name, $store_location +$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly) + +$module.Result.exists = $false +$module.Result.certificates = @() + +try { + if ($null -ne $thumbprint) { + $found_certs = $store.Certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $thumbprint, $false) + } + else { + $found_certs = $store.Certificates + } + + if ($found_certs.Count -gt 0) { + $module.Result.exists = $true + [array]$module.Result.certificates = $found_certs | ForEach-Object { Get-CertificateInfo -cert $_ } | Sort-Object -Property { $_.thumbprint } + } +} +finally { + $store.Close() +} + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_certificate_info.py b/ansible_collections/community/windows/plugins/modules/win_certificate_info.py new file mode 100644 index 000000000..494a6ef82 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_certificate_info.py @@ -0,0 +1,228 @@ +#!/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) + +DOCUMENTATION = r''' +--- +module: win_certificate_info +short_description: Get information on certificates from a Windows Certificate Store +description: +- Returns information about certificates in a Windows Certificate Store. +options: + thumbprint: + description: + - The thumbprint as a hex string of a certificate to find. + - When specified, filters the I(certificates) return value to a single certificate + - See the examples for how to format the thumbprint. + type: str + required: no + store_name: + description: + - The name of the store to search. + - See U(https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.x509certificates.storename) + for a list of built-in store names. + type: str + default: My + store_location: + description: + - The location of the store to search. + type: str + choices: [ CurrentUser, LocalMachine ] + default: LocalMachine +seealso: +- module: ansible.windows.win_certificate_store +author: +- Micah Hunsberger (@mhunsber) +''' + +EXAMPLES = r''' +- name: Obtain information about a particular certificate in the computer's personal store + community.windows.win_certificate_info: + thumbprint: BD7AF104CF1872BDB518D95C9534EA941665FD27 + register: mycert + +# thumbprint can also be lower case +- name: Obtain information about a particular certificate in the computer's personal store + community.windows.win_certificate_info: + thumbprint: bd7af104cf1872bdb518d95c9534ea941665fd27 + register: mycert + +- name: Obtain information about all certificates in the root store + community.windows.win_certificate_info: + store_name: Root + register: ca + +# Import a pfx and then get information on the certificates +- name: Import pfx certificate that is password protected + ansible.windows.win_certificate_store: + path: C:\Temp\cert.pfx + state: present + password: VeryStrongPasswordHere! + become: yes + become_method: runas + register: mycert + +- name: Obtain information on each certificate that was touched + community.windows.win_certificate_info: + thumbprint: "{{ item }}" + register: mycert_stats + loop: "{{ mycert.thumbprints }}" +''' + +RETURN = r''' +exists: + description: + - Whether any certificates were found in the store. + - When I(thumbprint) is specified, returns true only if the certificate mathing the thumbprint exists. + returned: success + type: bool + sample: true +certificates: + description: + - A list of information about certificates found in the store, sorted by thumbprint. + returned: success + type: list + elements: dict + contains: + archived: + description: Indicates that the certificate is archived. + type: bool + sample: false + dns_names: + description: Lists the registered dns names for the certificate. + type: list + elements: str + sample: [ '*.m.wikiquote.org', '*.wikipedia.org' ] + extensions: + description: The collection of the certificates extensions. + type: list + elements: dict + sample: [ + { + "critical": false, + "field": "Subject Key Identifier", + "value": "88 27 17 09 a9 b6 18 60 8b ec eb ba f6 47 59 c5 52 54 a3 b7" + }, + { + "critical": true, + "field": "Basic Constraints", + "value": "Subject Type=CA, Path Length Constraint=None" + }, + { + "critical": false, + "field": "Authority Key Identifier", + "value": "KeyID=2b d0 69 47 94 76 09 fe f4 6b 8d 2e 40 a6 f7 47 4d 7f 08 5e" + }, + { + "critical": false, + "field": "CRL Distribution Points", + "value": "[1]CRL Distribution Point: Distribution Point Name:Full Name:URL=http://crl.apple.com/root.crl" + }, + { + "critical": true, + "field": "Key Usage", + "value": "Digital Signature, Certificate Signing, Off-line CRL Signing, CRL Signing (86)" + }, + { + "critical": false, + "field": null, + "value": "05 00" + } + ] + friendly_name: + description: The associated alias for the certificate. + type: str + sample: Microsoft Root Authority + has_private_key: + description: Indicates that the certificate contains a private key. + type: bool + sample: false + intended_purposes: + description: lists the intended applications for the certificate. + returned: enhanced key usages extension exists. + type: list + sample: [ "Server Authentication" ] + is_ca: + description: Indicates that the certificate is a certificate authority (CA) certificate. + returned: basic constraints extension exists. + type: bool + sample: true + issued_by: + description: The certificate issuer's common name. + type: str + sample: Apple Root CA + issued_to: + description: The certificate's common name. + type: str + sample: Apple Worldwide Developer Relations Certification Authority + issuer: + description: The certificate issuer's distinguished name. + type: str + sample: 'CN=Apple Root CA, OU=Apple Certification Authority, O=Apple Inc., C=US' + key_usages: + description: + - Defines how the certificate key can be used. + - If this value is not defined, the key can be used for any purpose. + returned: key usages extension exists. + type: list + elements: str + sample: [ "CrlSign", "KeyCertSign", "DigitalSignature" ] + path_length_constraint: + description: + - The number of levels allowed in a certificates path. + - If this value is 0, the certificate does not have a restriction. + returned: basic constraints extension exists + type: int + sample: 0 + public_key: + description: The base64 encoded public key of the certificate. + type: str + cert_data: + description: The base64 encoded data of the entire certificate. + type: str + serial_number: + description: The serial number of the certificate represented as a hexadecimal string + type: str + sample: 01DEBCC4396DA010 + signature_algorithm: + description: The algorithm used to create the certificate's signature + type: str + sample: sha1RSA + ski: + description: The certificate's subject key identifier + returned: subject key identifier extension exists. + type: str + sample: 88271709A9B618608BECEBBAF64759C55254A3B7 + subject: + description: The certificate's distinguished name. + type: str + sample: 'CN=Apple Worldwide Developer Relations Certification Authority, OU=Apple Worldwide Developer Relations, O=Apple Inc., C=US' + thumbprint: + description: + - The thumbprint as a hex string of the certificate. + - The return format will always be upper case. + type: str + sample: FF6797793A3CD798DC5B2ABEF56F73EDC9F83A64 + valid_from: + description: The start date of the certificate represented in seconds since epoch. + type: float + sample: 1360255727 + valid_from_iso8601: + description: The start date of the certificate represented as an iso8601 formatted date. + type: str + sample: '2017-12-15T08:39:32Z' + valid_to: + description: The expiry date of the certificate represented in seconds since epoch. + type: float + sample: 1675788527 + valid_to_iso8601: + description: The expiry date of the certificate represented as an iso8601 formatted date. + type: str + sample: '2086-01-02T08:39:32Z' + version: + description: The x509 format version of the certificate + type: int + sample: 3 +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_computer_description.ps1 b/ansible_collections/community/windows/plugins/modules/win_computer_description.ps1 new file mode 100644 index 000000000..445e5c2a2 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_computer_description.ps1 @@ -0,0 +1,54 @@ +#!powershell + +# Copyright: (c) 2019, RusoSova +# 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.1 + +$spec = @{ + options = @{ + owner = @{ type = "str" } + organization = @{ type = "str" } + description = @{ type = "str" } + } + required_one_of = @( + , @('owner', 'organization', 'description') + ) + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$owner = $module.Params.owner +$organization = $module.Params.organization +$description = $module.Params.description +$regPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\" + +#Change description +if ($description -or $description -eq "") { + $descriptionObject = Get-CimInstance -class "Win32_OperatingSystem" + if ($description -cne $descriptionObject.description) { + Set-CimInstance -InputObject $descriptionObject -Property @{"Description" = "$description" } -WhatIf:$module.CheckMode + $module.Result.changed = $true + } +} + +#Change owner +if ($owner -or $owner -eq "") { + $curentOwner = (Get-ItemProperty -LiteralPath $regPath -Name RegisteredOwner).RegisteredOwner + if ($curentOwner -cne $owner) { + Set-ItemProperty -LiteralPath $regPath -Name "RegisteredOwner" -Value $owner -WhatIf:$module.CheckMode + $module.Result.changed = $true + } +} + +#Change organization +if ($organization -or $organization -eq "") { + $curentOrganization = (Get-ItemProperty -LiteralPath $regPath -Name RegisteredOrganization).RegisteredOrganization + if ($curentOrganization -cne $organization) { + Set-ItemProperty -LiteralPath $regPath -Name "RegisteredOrganization" -Value $organization -WhatIf:$module.CheckMode + $module.Result.changed = $true + } +} +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_computer_description.py b/ansible_collections/community/windows/plugins/modules/win_computer_description.py new file mode 100644 index 000000000..48eff8864 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_computer_description.py @@ -0,0 +1,62 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, RusoSova +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_computer_description +short_description: Set windows description, owner and organization +description: + - This module sets Windows description that is shown under My Computer properties. Module also sets + Windows license owner and organization. License information can be viewed by running winver commad. +options: + description: + description: + - String value to apply to Windows descripton. Specify value of "" to clear the value. + required: false + type: str + organization: + description: + - String value of organization that the Windows is licensed to. Specify value of "" to clear the value. + required: false + type: str + owner: + description: + - String value of the persona that the Windows is licensed to. Specify value of "" to clear the value. + required: false + type: str +author: + - RusoSova (@RusoSova) +''' + +EXAMPLES = r''' +- name: Set Windows description, owner and organization + community.windows.win_computer_description: + description: Best Box + owner: RusoSova + organization: MyOrg + register: result + +- name: Set Windows description only + community.windows.win_computer_description: + description: This is my Windows machine + register: result + +- name: Set organization and clear owner field + community.windows.win_computer_description: + owner: '' + organization: Black Mesa + +- name: Clear organization, description and owner + community.windows.win_computer_description: + organization: "" + owner: "" + description: "" + register: result +''' + +RETURN = r''' +# +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_credential.ps1 b/ansible_collections/community/windows/plugins/modules/win_credential.ps1 new file mode 100644 index 000000000..b39bff57e --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_credential.ps1 @@ -0,0 +1,724 @@ +#!powershell + +# Copyright: (c) 2018, 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.AddType + +$spec = @{ + options = @{ + alias = @{ type = "str" } + attributes = @{ + type = "list" + elements = "dict" + options = @{ + name = @{ type = "str"; required = $true } + data = @{ type = "str" } + data_format = @{ type = "str"; default = "text"; choices = @("base64", "text") } + } + } + comment = @{ type = "str" } + name = @{ type = "str"; required = $true } + persistence = @{ type = "str"; default = "local"; choices = @("enterprise", "local") } + secret = @{ type = "str"; no_log = $true } + secret_format = @{ type = "str"; default = "text"; choices = @("base64", "text") } + state = @{ type = "str"; default = "present"; choices = @("absent", "present") } + type = @{ + type = "str" + required = $true + choices = @("domain_password", "domain_certificate", "generic_password", "generic_certificate") + } + update_secret = @{ type = "str"; default = "always"; choices = @("always", "on_create") } + username = @{ type = "str" } + } + required_if = @( + , @("state", "present", @("username")) + ) + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$alias = $module.Params.alias +$attributes = $module.Params.attributes +$comment = $module.Params.comment +$name = $module.Params.name +$persistence = $module.Params.persistence +$secret = $module.Params.secret +$secret_format = $module.Params.secret_format +$state = $module.Params.state +$type = $module.Params.type +$update_secret = $module.Params.update_secret +$username = $module.Params.username + +$module.Diff.before = "" +$module.Diff.after = "" + +Add-CSharpType -AnsibleModule $module -References @' +using Microsoft.Win32.SafeHandles; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ansible.CredentialManager +{ + internal class NativeHelpers + { + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public class CREDENTIAL + { + public CredentialFlags Flags; + public CredentialType Type; + [MarshalAs(UnmanagedType.LPWStr)] public string TargetName; + [MarshalAs(UnmanagedType.LPWStr)] public string Comment; + public FILETIME LastWritten; + public UInt32 CredentialBlobSize; + public IntPtr CredentialBlob; + public CredentialPersist Persist; + public UInt32 AttributeCount; + public IntPtr Attributes; + [MarshalAs(UnmanagedType.LPWStr)] public string TargetAlias; + [MarshalAs(UnmanagedType.LPWStr)] public string UserName; + + public static explicit operator Credential(CREDENTIAL v) + { + byte[] secret = new byte[(int)v.CredentialBlobSize]; + if (v.CredentialBlob != IntPtr.Zero) + Marshal.Copy(v.CredentialBlob, secret, 0, secret.Length); + + List<CredentialAttribute> attributes = new List<CredentialAttribute>(); + if (v.AttributeCount > 0) + { + CREDENTIAL_ATTRIBUTE[] rawAttributes = new CREDENTIAL_ATTRIBUTE[v.AttributeCount]; + Credential.PtrToStructureArray(rawAttributes, v.Attributes); + attributes = rawAttributes.Select(x => (CredentialAttribute)x).ToList(); + } + + string userName = v.UserName; + if (v.Type == CredentialType.DomainCertificate || v.Type == CredentialType.GenericCertificate) + userName = Credential.UnmarshalCertificateCredential(userName); + + return new Credential + { + Type = v.Type, + TargetName = v.TargetName, + Comment = v.Comment, + LastWritten = (DateTimeOffset)v.LastWritten, + Secret = secret, + Persist = v.Persist, + Attributes = attributes, + TargetAlias = v.TargetAlias, + UserName = userName, + Loaded = true, + }; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct CREDENTIAL_ATTRIBUTE + { + [MarshalAs(UnmanagedType.LPWStr)] public string Keyword; + public UInt32 Flags; // Set to 0 and is reserved + public UInt32 ValueSize; + public IntPtr Value; + + public static explicit operator CredentialAttribute(CREDENTIAL_ATTRIBUTE v) + { + byte[] value = new byte[v.ValueSize]; + Marshal.Copy(v.Value, value, 0, (int)v.ValueSize); + + return new CredentialAttribute + { + Keyword = v.Keyword, + Flags = v.Flags, + Value = value, + }; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct FILETIME + { + internal UInt32 dwLowDateTime; + internal UInt32 dwHighDateTime; + + public static implicit operator long(FILETIME v) { return ((long)v.dwHighDateTime << 32) + v.dwLowDateTime; } + public static explicit operator DateTimeOffset(FILETIME v) { return DateTimeOffset.FromFileTime(v); } + public static explicit operator FILETIME(DateTimeOffset v) + { + return new FILETIME() + { + dwLowDateTime = (UInt32)v.ToFileTime(), + dwHighDateTime = ((UInt32)v.ToFileTime() >> 32), + }; + } + } + + [Flags] + public enum CredentialCreateFlags : uint + { + PreserveCredentialBlob = 1, + } + + [Flags] + public enum CredentialFlags + { + None = 0, + PromptNow = 2, + UsernameTarget = 4, + } + + public enum CredMarshalType : uint + { + CertCredential = 1, + UsernameTargetCredential, + BinaryBlobCredential, + UsernameForPackedCredential, + BinaryBlobForSystem, + } + } + + internal class NativeMethods + { + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool CredDeleteW( + [MarshalAs(UnmanagedType.LPWStr)] string TargetName, + CredentialType Type, + UInt32 Flags); + + [DllImport("advapi32.dll")] + public static extern void CredFree( + IntPtr Buffer); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool CredMarshalCredentialW( + NativeHelpers.CredMarshalType CredType, + SafeMemoryBuffer Credential, + out SafeCredentialBuffer MarshaledCredential); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool CredReadW( + [MarshalAs(UnmanagedType.LPWStr)] string TargetName, + CredentialType Type, + UInt32 Flags, + out SafeCredentialBuffer Credential); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool CredUnmarshalCredentialW( + [MarshalAs(UnmanagedType.LPWStr)] string MarshaledCredential, + out NativeHelpers.CredMarshalType CredType, + out SafeCredentialBuffer Credential); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool CredWriteW( + NativeHelpers.CREDENTIAL Credential, + NativeHelpers.CredentialCreateFlags Flags); + } + + internal class SafeCredentialBuffer : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeCredentialBuffer() : base(true) { } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + protected override bool ReleaseHandle() + { + NativeMethods.CredFree(handle); + return true; + } + } + + internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeMemoryBuffer() : base(true) { } + public SafeMemoryBuffer(int cb) : base(true) + { + 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; + } + } + + public class Win32Exception : System.ComponentModel.Win32Exception + { + private string _exception_msg; + public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { } + public Win32Exception(int errorCode, string message) : base(errorCode) + { + _exception_msg = String.Format("{0} - {1} (Win32 Error Code {2}: 0x{3})", message, base.Message, errorCode, errorCode.ToString("X8")); + } + public override string Message { get { return _exception_msg; } } + public static explicit operator Win32Exception(string message) { return new Win32Exception(message); } + } + + public enum CredentialPersist + { + Session = 1, + LocalMachine = 2, + Enterprise = 3, + } + + public enum CredentialType + { + Generic = 1, + DomainPassword = 2, + DomainCertificate = 3, + DomainVisiblePassword = 4, + GenericCertificate = 5, + DomainExtended = 6, + Maximum = 7, + MaximumEx = 1007, + } + + public class CredentialAttribute + { + public string Keyword; + public UInt32 Flags; + public byte[] Value; + } + + public class Credential + { + public CredentialType Type; + public string TargetName; + public string Comment; + public DateTimeOffset LastWritten; + public byte[] Secret; + public CredentialPersist Persist; + public List<CredentialAttribute> Attributes = new List<CredentialAttribute>(); + public string TargetAlias; + public string UserName; + + // Used to track whether the credential has been loaded into the store or not + public bool Loaded { get; internal set; } + + public void Delete() + { + if (!Loaded) + return; + + if (!NativeMethods.CredDeleteW(TargetName, Type, 0)) + throw new Win32Exception(String.Format("CredDeleteW({0}) failed", TargetName)); + Loaded = false; + } + + public void Write(bool preserveExisting) + { + string userName = UserName; + // Convert the certificate thumbprint to the string expected + if (Type == CredentialType.DomainCertificate || Type == CredentialType.GenericCertificate) + userName = Credential.MarshalCertificateCredential(userName); + + NativeHelpers.CREDENTIAL credential = new NativeHelpers.CREDENTIAL + { + Flags = NativeHelpers.CredentialFlags.None, + Type = Type, + TargetName = TargetName, + Comment = Comment, + LastWritten = new NativeHelpers.FILETIME(), + CredentialBlobSize = (UInt32)(Secret == null ? 0 : Secret.Length), + CredentialBlob = IntPtr.Zero, // Must be allocated and freed outside of this to ensure no memory leaks + Persist = Persist, + AttributeCount = (UInt32)(Attributes.Count), + Attributes = IntPtr.Zero, // Attributes must be allocated and freed outside of this to ensure no memory leaks + TargetAlias = TargetAlias, + UserName = userName, + }; + + using (SafeMemoryBuffer credentialBlob = new SafeMemoryBuffer((int)credential.CredentialBlobSize)) + { + if (Secret != null) + Marshal.Copy(Secret, 0, credentialBlob.DangerousGetHandle(), Secret.Length); + credential.CredentialBlob = credentialBlob.DangerousGetHandle(); + + // Store the CREDENTIAL_ATTRIBUTE value in a safe memory buffer and make sure we dispose in all cases + List<SafeMemoryBuffer> attributeBuffers = new List<SafeMemoryBuffer>(); + try + { + int attributeLength = Attributes.Sum(a => Marshal.SizeOf(typeof(NativeHelpers.CREDENTIAL_ATTRIBUTE))); + byte[] attributeBytes = new byte[attributeLength]; + int offset = 0; + foreach (CredentialAttribute attribute in Attributes) + { + SafeMemoryBuffer attributeBuffer = new SafeMemoryBuffer(attribute.Value.Length); + attributeBuffers.Add(attributeBuffer); + if (attribute.Value != null) + Marshal.Copy(attribute.Value, 0, attributeBuffer.DangerousGetHandle(), attribute.Value.Length); + + NativeHelpers.CREDENTIAL_ATTRIBUTE credentialAttribute = new NativeHelpers.CREDENTIAL_ATTRIBUTE + { + Keyword = attribute.Keyword, + Flags = attribute.Flags, + ValueSize = (UInt32)(attribute.Value == null ? 0 : attribute.Value.Length), + Value = attributeBuffer.DangerousGetHandle(), + }; + int attributeStructLength = Marshal.SizeOf(typeof(NativeHelpers.CREDENTIAL_ATTRIBUTE)); + + byte[] attrBytes = new byte[attributeStructLength]; + using (SafeMemoryBuffer tempBuffer = new SafeMemoryBuffer(attributeStructLength)) + { + Marshal.StructureToPtr(credentialAttribute, tempBuffer.DangerousGetHandle(), false); + Marshal.Copy(tempBuffer.DangerousGetHandle(), attrBytes, 0, attributeStructLength); + } + Buffer.BlockCopy(attrBytes, 0, attributeBytes, offset, attributeStructLength); + offset += attributeStructLength; + } + + using (SafeMemoryBuffer attributes = new SafeMemoryBuffer(attributeBytes.Length)) + { + if (attributeBytes.Length != 0) + { + Marshal.Copy(attributeBytes, 0, attributes.DangerousGetHandle(), attributeBytes.Length); + credential.Attributes = attributes.DangerousGetHandle(); + } + + NativeHelpers.CredentialCreateFlags createFlags = 0; + if (preserveExisting) + createFlags |= NativeHelpers.CredentialCreateFlags.PreserveCredentialBlob; + + if (!NativeMethods.CredWriteW(credential, createFlags)) + throw new Win32Exception(String.Format("CredWriteW({0}) failed", TargetName)); + } + } + finally + { + foreach (SafeMemoryBuffer attributeBuffer in attributeBuffers) + attributeBuffer.Dispose(); + } + } + Loaded = true; + } + + public static Credential GetCredential(string target, CredentialType type) + { + SafeCredentialBuffer buffer; + if (!NativeMethods.CredReadW(target, type, 0, out buffer)) + { + int lastErr = Marshal.GetLastWin32Error(); + + // Not running with Become so cannot manage the user's credentials + if (lastErr == 0x00000520) // ERROR_NO_SUCH_LOGON_SESSION + throw new InvalidOperationException("Failed to access the user's credential store, run the module with become"); + else if (lastErr == 0x00000490) // ERROR_NOT_FOUND + return null; + throw new Win32Exception(lastErr, "CredEnumerateW() failed"); + } + + using (buffer) + { + NativeHelpers.CREDENTIAL credential = (NativeHelpers.CREDENTIAL)Marshal.PtrToStructure( + buffer.DangerousGetHandle(), typeof(NativeHelpers.CREDENTIAL)); + return (Credential)credential; + } + } + + public static string MarshalCertificateCredential(string thumbprint) + { + // CredWriteW requires the UserName field to be the value of CredMarshalCredentialW() when writting a + // certificate auth. This converts the UserName property to the format required. + + // While CERT_CREDENTIAL_INFO is the correct structure, we manually marshal the data in order to + // support different cert hash lengths in the future. + // https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_cert_credential_info + int hexLength = thumbprint.Length; + byte[] credInfo = new byte[sizeof(UInt32) + (hexLength / 2)]; + + // First field is cbSize which is a UInt32 value denoting the size of the total structure + Array.Copy(BitConverter.GetBytes((UInt32)credInfo.Length), credInfo, sizeof(UInt32)); + + // Now copy the byte representation of the thumbprint to the rest of the struct bytes + for (int i = 0; i < hexLength; i += 2) + credInfo[sizeof(UInt32) + (i / 2)] = Convert.ToByte(thumbprint.Substring(i, 2), 16); + + IntPtr pCredInfo = Marshal.AllocHGlobal(credInfo.Length); + Marshal.Copy(credInfo, 0, pCredInfo, credInfo.Length); + SafeMemoryBuffer pCredential = new SafeMemoryBuffer(pCredInfo); + + NativeHelpers.CredMarshalType marshalType = NativeHelpers.CredMarshalType.CertCredential; + using (pCredential) + { + SafeCredentialBuffer marshaledCredential; + if (!NativeMethods.CredMarshalCredentialW(marshalType, pCredential, out marshaledCredential)) + throw new Win32Exception("CredMarshalCredentialW() failed"); + using (marshaledCredential) + return Marshal.PtrToStringUni(marshaledCredential.DangerousGetHandle()); + } + } + + public static string UnmarshalCertificateCredential(string value) + { + NativeHelpers.CredMarshalType credType; + SafeCredentialBuffer pCredInfo; + if (!NativeMethods.CredUnmarshalCredentialW(value, out credType, out pCredInfo)) + throw new Win32Exception("CredUnmarshalCredentialW() failed"); + + using (pCredInfo) + { + if (credType != NativeHelpers.CredMarshalType.CertCredential) + throw new InvalidOperationException(String.Format("Expected unmarshalled cred type of CertCredential, received {0}", credType)); + + byte[] structSizeBytes = new byte[sizeof(UInt32)]; + Marshal.Copy(pCredInfo.DangerousGetHandle(), structSizeBytes, 0, sizeof(UInt32)); + UInt32 structSize = BitConverter.ToUInt32(structSizeBytes, 0); + + byte[] certInfoBytes = new byte[structSize]; + Marshal.Copy(pCredInfo.DangerousGetHandle(), certInfoBytes, 0, certInfoBytes.Length); + + StringBuilder hex = new StringBuilder((certInfoBytes.Length - sizeof(UInt32)) * 2); + for (int i = 4; i < certInfoBytes.Length; i++) + hex.AppendFormat("{0:x2}", certInfoBytes[i]); + + return hex.ToString().ToUpperInvariant(); + } + } + + internal static void PtrToStructureArray<T>(T[] array, IntPtr ptr) + { + IntPtr ptrOffset = ptr; + for (int i = 0; i < array.Length; i++, ptrOffset = IntPtr.Add(ptrOffset, Marshal.SizeOf(typeof(T)))) + array[i] = (T)Marshal.PtrToStructure(ptrOffset, typeof(T)); + } + } +} +'@ + +Function ConvertTo-CredentialAttribute { + param($Attributes) + + $converted_attributes = [System.Collections.Generic.List`1[Ansible.CredentialManager.CredentialAttribute]]@() + foreach ($attribute in $Attributes) { + $new_attribute = New-Object -TypeName Ansible.CredentialManager.CredentialAttribute + $new_attribute.Keyword = $attribute.name + + if ($null -ne $attribute.data) { + if ($attribute.data_format -eq "base64") { + $new_attribute.Value = [System.Convert]::FromBase64String($attribute.data) + } + else { + $new_attribute.Value = [System.Text.Encoding]::UTF8.GetBytes($attribute.data) + } + } + $converted_attributes.Add($new_attribute) > $null + } + + return , $converted_attributes +} + +Function Get-DiffInfo { + param($AnsibleCredential) + + $diff = @{ + alias = $AnsibleCredential.TargetAlias + attributes = [System.Collections.ArrayList]@() + comment = $AnsibleCredential.Comment + name = $AnsibleCredential.TargetName + persistence = $AnsibleCredential.Persist.ToString() + type = $AnsibleCredential.Type.ToString() + username = $AnsibleCredential.UserName + } + + foreach ($attribute in $AnsibleCredential.Attributes) { + $attribute_info = @{ + name = $attribute.Keyword + data = $null + } + if ($null -ne $attribute.Value) { + $attribute_info.data = [System.Convert]::ToBase64String($attribute.Value) + } + $diff.attributes.Add($attribute_info) > $null + } + + return , $diff +} + +# If the username is a certificate thumbprint, verify it's a valid cert in the CurrentUser/Personal store +if ($null -ne $username -and $type -in @("domain_certificate", "generic_certificate")) { + # Ensure the thumbprint is upper case with no spaces or hyphens + $username = $username.ToUpperInvariant().Replace(" ", "").Replace("-", "") + + $certificate = Get-Item -LiteralPath Cert:\CurrentUser\My\$username -ErrorAction SilentlyContinue + if ($null -eq $certificate) { + $module.FailJson("Failed to find certificate with the thumbprint $username in the CurrentUser\My store") + } +} + +# Convert the input secret to a byte array +if ($null -ne $secret) { + if ($secret_format -eq "base64") { + $secret = [System.Convert]::FromBase64String($secret) + } + else { + $secret = [System.Text.Encoding]::Unicode.GetBytes($secret) + } +} + +$persistence = switch ($persistence) { + "local" { [Ansible.CredentialManager.CredentialPersist]::LocalMachine } + "enterprise" { [Ansible.CredentialManager.CredentialPersist]::Enterprise } +} + +$type = switch ($type) { + "domain_password" { [Ansible.CredentialManager.CredentialType]::DomainPassword } + "domain_certificate" { [Ansible.CredentialManager.CredentialType]::DomainCertificate } + "generic_password" { [Ansible.CredentialManager.CredentialType]::Generic } + "generic_certificate" { [Ansible.CredentialManager.CredentialType]::GenericCertificate } +} + +$existing_credential = [Ansible.CredentialManager.Credential]::GetCredential($name, $type) +if ($null -ne $existing_credential) { + $module.Diff.before = Get-DiffInfo -AnsibleCredential $existing_credential +} + +if ($state -eq "absent") { + if ($null -ne $existing_credential) { + if (-not $module.CheckMode) { + $existing_credential.Delete() + } + $module.Result.changed = $true + } +} +else { + if ($null -eq $existing_credential) { + $new_credential = New-Object -TypeName Ansible.CredentialManager.Credential + $new_credential.Type = $type + $new_credential.TargetName = $name + $new_credential.Comment = if ($comment) { $comment } else { [NullString]::Value } + $new_credential.Secret = $secret + $new_credential.Persist = $persistence + $new_credential.TargetAlias = if ($alias) { $alias } else { [NullString]::Value } + $new_credential.UserName = $username + + if ($null -ne $attributes) { + $new_credential.Attributes = ConvertTo-CredentialAttribute -Attributes $attributes + } + + if (-not $module.CheckMode) { + $new_credential.Write($false) + } + $module.Result.changed = $true + } + else { + $changed = $false + $preserve_blob = $false + + # make sure we do case comparison for the comment + if ($existing_credential.Comment -cne $comment) { + $existing_credential.Comment = $comment + $changed = $true + } + + if ($existing_credential.Persist -ne $persistence) { + $existing_credential.Persist = $persistence + $changed = $true + } + + if ($existing_credential.TargetAlias -ne $alias) { + $existing_credential.TargetAlias = $alias + $changed = $true + } + + if ($existing_credential.UserName -ne $username) { + $existing_credential.UserName = $username + $changed = $true + } + + if ($null -ne $attributes) { + $attribute_changed = $false + + $new_attributes = ConvertTo-CredentialAttribute -Attributes $attributes + if ($new_attributes.Count -ne $existing_credential.Attributes.Count) { + $attribute_changed = $true + } + else { + for ($i = 0; $i -lt $new_attributes.Count; $i++) { + $new_keyword = $new_attributes[$i].Keyword + $new_value = $new_attributes[$i].Value + if ($null -eq $new_value) { + $new_value = "" + } + else { + $new_value = [System.Convert]::ToBase64String($new_value) + } + + $existing_keyword = $existing_credential.Attributes[$i].Keyword + $existing_value = $existing_credential.Attributes[$i].Value + if ($null -eq $existing_value) { + $existing_value = "" + } + else { + $existing_value = [System.Convert]::ToBase64String($existing_value) + } + + if (($new_keyword -cne $existing_keyword) -or ($new_value -ne $existing_value)) { + $attribute_changed = $true + break + } + } + } + + if ($attribute_changed) { + $existing_credential.Attributes = $new_attributes + $changed = $true + } + } + + if ($null -eq $secret) { + # If we haven't explicitly set a secret, tell Windows to preserve the existing blob + $preserve_blob = $true + $existing_credential.Secret = $null + } + elseif ($update_secret -eq "always") { + # We should only set the password if we can't read the existing one or it doesn't match our secret + if ($existing_credential.Secret.Length -eq 0) { + # We cannot read the secret so don't know if its the configured secret + $existing_credential.Secret = $secret + $changed = $true + } + else { + # We can read the secret so compare with our input + $input_secret_b64 = [System.Convert]::ToBase64String($secret) + $actual_secret_b64 = [System.Convert]::ToBase64String($existing_credential.Secret) + if ($input_secret_b64 -ne $actual_secret_b64) { + $existing_credential.Secret = $secret + $changed = $true + } + } + } + + if ($changed -and -not $module.CheckMode) { + $existing_credential.Write($preserve_blob) + } + $module.Result.changed = $changed + } + + if ($module.CheckMode) { + # We cannot reliably get the credential in check mode, set it based on the input + $module.Diff.after = @{ + alias = $alias + attributes = $attributes + comment = $comment + name = $name + persistence = $persistence.ToString() + type = $type.ToString() + username = $username + } + } + else { + # Get a new copy of the credential and use that to set the after diff + $new_credential = [Ansible.CredentialManager.Credential]::GetCredential($name, $type) + $module.Diff.after = Get-DiffInfo -AnsibleCredential $new_credential + } +} + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_credential.py b/ansible_collections/community/windows/plugins/modules/win_credential.py new file mode 100644 index 000000000..fd605f0d0 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_credential.py @@ -0,0 +1,206 @@ +#!/usr/bin/python +# -*- 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) + +DOCUMENTATION = r''' +--- +module: win_credential +short_description: Manages Windows Credentials in the Credential Manager +description: +- Used to create and remove Windows Credentials in the Credential Manager. +- This module can manage both standard username/password credentials as well as + certificate credentials. +options: + alias: + description: + - Adds an alias for the credential. + - Typically this is the NetBIOS name of a host if I(name) is set to the DNS + name. + type: str + attributes: + description: + - A list of dicts that set application specific attributes for a + credential. + - When set, existing attributes will be compared to the list as a whole, + any differences means all attributes will be replaced. + type: list + elements: dict + suboptions: + name: + description: + - The key for the attribute. + - This is not a unique identifier as multiple attributes can have the + same key. + type: str + required: true + data: + description: + - The value for the attribute. + type: str + data_format: + description: + - Controls the input type for I(data). + - If C(text), I(data) is a text string that is UTF-16LE encoded to + bytes. + - If C(base64), I(data) is a base64 string that is base64 decoded to + bytes. + type: str + choices: [ base64, text ] + default: text + comment: + description: + - A user defined comment for the credential. + type: str + name: + description: + - The target that identifies the server or servers that the credential is + to be used for. + - If the value can be a NetBIOS name, DNS server name, DNS host name suffix + with a wildcard character (C(*)), a NetBIOS of DNS domain name that + contains a wildcard character sequence, or an asterisk. + - See C(TargetName) in U(https://docs.microsoft.com/en-us/windows/win32/api/wincred/ns-wincred-credentiala) + for more details on what this value can be. + - This is used with I(type) to produce a unique credential. + type: str + required: true + persistence: + description: + - Defines the persistence of the credential. + - If C(local), the credential will persist for all logons of the same user + on the same host. + - C(enterprise) is the same as C(local) but the credential is visible to + the same domain user when running on other hosts and not just localhost. + type: str + choices: [ enterprise, local ] + default: local + secret: + description: + - The secret for the credential. + - When omitted, then no secret is used for the credential if a new + credentials is created. + - When I(type) is a password type, this is the password for I(username). + - When I(type) is a certificate type, this is the pin for the certificate. + type: str + secret_format: + description: + - Controls the input type for I(secret). + - If C(text), I(secret) is a text string that is UTF-16LE encoded to bytes. + - If C(base64), I(secret) is a base64 string that is base64 decoded to + bytes. + type: str + choices: [ base64, text ] + default: text + state: + description: + - When C(absent), the credential specified by I(name) and I(type) is + removed. + - When C(present), the credential specified by I(name) and I(type) is + removed. + type: str + choices: [ absent, present ] + default: present + type: + description: + - The type of credential to store. + - This is used with I(name) to produce a unique credential. + - When the type is a C(domain) type, the credential is used by Microsoft + authentication packages like Negotiate. + - When the type is a C(generic) type, the credential is not used by any + particular authentication package. + - It is recommended to use a C(domain) type as only authentication + providers can access the secret. + type: str + required: true + choices: [ domain_certificate, domain_password, generic_certificate, generic_password ] + update_secret: + description: + - When C(always), the secret will always be updated if they differ. + - When C(on_create), the secret will only be checked/updated when it is + first created. + - If the secret cannot be retrieved and this is set to C(always), the + module will always result in a change. + type: str + choices: [ always, on_create ] + default: always + username: + description: + - When I(type) is a password type, then this is the username to store for + the credential. + - When I(type) is a credential type, then this is the thumbprint as a hex + string of the certificate to use. + - When C(type=domain_password), this should be in the form of a Netlogon + (DOMAIN\Username) or a UPN (username@DOMAIN). + - If using a certificate thumbprint, the certificate must exist in the + C(CurrentUser\My) certificate store for the executing user. + type: str +notes: +- This module requires to be run with C(become) so it can access the + user's credential store. +- There can only be one credential per host and type. if a second credential is + defined that uses the same host and type, then the original credential is + overwritten. +seealso: +- module: ansible.windows.win_user_right +- module: ansible.windows.win_whoami +author: +- Jordan Borean (@jborean93) +''' + +EXAMPLES = r''' +- name: Create a local only credential + community.windows.win_credential: + name: server.domain.com + type: domain_password + username: DOMAIN\username + secret: Password01 + state: present + +- name: Remove a credential + community.windows.win_credential: + name: server.domain.com + type: domain_password + state: absent + +- name: Create a credential with full values + community.windows.win_credential: + name: server.domain.com + type: domain_password + alias: server + username: username@DOMAIN.COM + secret: Password01 + comment: Credential for server.domain.com + persistence: enterprise + attributes: + - name: Source + data: Ansible + - name: Unique Identifier + data: Y3VzdG9tIGF0dHJpYnV0ZQ== + data_format: base64 + +- name: Create a certificate credential + community.windows.win_credential: + name: '*.domain.com' + type: domain_certificate + username: 0074CC4F200D27DC3877C24A92BA8EA21E6C7AF4 + state: present + +- name: Create a generic credential + community.windows.win_credential: + name: smbhost + type: generic_password + username: smbuser + secret: smbuser + state: present + +- name: Remove a generic credential + community.windows.win_credential: + name: smbhost + type: generic_password + state: absent +''' + +RETURN = r''' +# +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_data_deduplication.ps1 b/ansible_collections/community/windows/plugins/modules/win_data_deduplication.ps1 new file mode 100644 index 000000000..b326ea541 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_data_deduplication.ps1 @@ -0,0 +1,132 @@ +#!powershell + +# Copyright: 2019, rnsc(@rnsc) <github@rnsc.be> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt + +#AnsibleRequires -CSharpUtil Ansible.Basic +#AnsibleRequires -OSVersion 6.3 + +$spec = @{ + options = @{ + drive_letter = @{ type = "str"; required = $true } + state = @{ type = "str"; choices = "absent", "present"; default = "present"; } + settings = @{ + type = "dict" + required = $false + options = @{ + minimum_file_size = @{ type = "int"; default = 32768 } + minimum_file_age_days = @{ type = "int"; default = 2 } + no_compress = @{ type = "bool"; required = $false; default = $false } + optimize_in_use_files = @{ type = "bool"; required = $false; default = $false } + verify = @{ type = "bool"; required = $false; default = $false } + } + } + } + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$drive_letter = $module.Params.drive_letter +$state = $module.Params.state +$settings = $module.Params.settings + +$module.Result.changed = $false +$module.Result.reboot_required = $false +$module.Result.msg = "" + +function Set-DataDeduplication($volume, $state, $settings, $dedup_job) { + + $current_state = 'absent' + + try { + $dedup_info = Get-DedupVolume -Volume "$($volume.DriveLetter):" + } + catch { + $dedup_info = $null + } + + if ($dedup_info.Enabled) { + $current_state = 'present' + } + + if ( $state -ne $current_state ) { + if ( -not $module.CheckMode) { + if ($state -eq 'present') { + # Enable-DedupVolume -Volume <String> + Enable-DedupVolume -Volume "$($volume.DriveLetter):" + } + elseif ($state -eq 'absent') { + Disable-DedupVolume -Volume "$($volume.DriveLetter):" + } + } + $module.Result.changed = $true + } + + if ($state -eq 'present') { + if ($null -ne $settings) { + Set-DataDedupJobSetting -volume $volume -settings $settings + } + } +} + +function Set-DataDedupJobSetting ($volume, $settings) { + + try { + $dedup_info = Get-DedupVolume -Volume "$($volume.DriveLetter):" + } + catch { + $dedup_info = $null + } + + ForEach ($key in $settings.keys) { + + # See Microsoft documentation: + # https://docs.microsoft.com/en-us/powershell/module/deduplication/set-dedupvolume?view=win10-ps + + $update_key = $key + $update_value = $settings.$($key) + # Transform Ansible style options to Powershell params + $update_key = $update_key -replace ('_', '') + + if ($update_key -eq "MinimumFileSize" -and $update_value -lt 32768) { + $update_value = 32768 + } + + $current_value = ($dedup_info | Select-Object -ExpandProperty $update_key) + + if ($update_value -ne $current_value) { + $command_param = @{ + $($update_key) = $update_value + } + + # Set-DedupVolume -Volume <String>` + # -NoCompress <bool> ` + # -MinimumFileAgeDays <UInt32> ` + # -MinimumFileSize <UInt32> (minimum 32768) + if ( -not $module.CheckMode ) { + Set-DedupVolume -Volume "$($volume.DriveLetter):" @command_param + } + + $module.Result.changed = $true + } + } + +} + +# Install required feature +$feature_name = "FS-Data-Deduplication" +if ( -not $module.CheckMode) { + $feature = Install-WindowsFeature -Name $feature_name + + if ($feature.RestartNeeded -eq 'Yes') { + $module.Result.reboot_required = $true + $module.FailJson("$feature_name was installed but requires Windows to be rebooted to work.") + } +} + +$volume = Get-Volume -DriveLetter $drive_letter + +Set-DataDeduplication -volume $volume -state $state -settings $settings -dedup_job $dedup_job + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_data_deduplication.py b/ansible_collections/community/windows/plugins/modules/win_data_deduplication.py new file mode 100644 index 000000000..f43474628 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_data_deduplication.py @@ -0,0 +1,82 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: 2019, rnsc(@rnsc) <github@rnsc.be> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_data_deduplication +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 + community.windows.win_data_deduplication: + drive_letter: 'D' + state: present + +- name: Enable Data Deduplication on D + community.windows.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/ansible_collections/community/windows/plugins/modules/win_defrag.ps1 b/ansible_collections/community/windows/plugins/modules/win_defrag.ps1 new file mode 100644 index 000000000..067e49086 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_defrag.ps1 @@ -0,0 +1,101 @@ +#!powershell + +# Copyright: (c) 2017, Dag Wieers (@dagwieers) <dag@wieers.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic +#Requires -Module Ansible.ModuleUtils.ArgvParser +#Requires -Module Ansible.ModuleUtils.CommandUtil + +$spec = @{ + options = @{ + include_volumes = @{ type = 'list'; elements = 'str' } + exclude_volumes = @{ type = 'list'; elements = 'str' } + freespace_consolidation = @{ type = 'bool'; default = $false } + priority = @{ type = 'str'; default = 'low'; choices = @( 'low', 'normal') } + parallel = @{ type = 'bool'; default = $false } + } + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$include_volumes = $module.Params.include_volumes +$exclude_volumes = $module.Params.exclude_volumes +$freespace_consolidation = $module.Params.freespace_consolidation +$priority = $module.Params.priority +$parallel = $module.Params.parallel + +$module.Result.changed = $false + +$executable = "defrag.exe" + +if (-not (Get-Command -Name $executable -ErrorAction SilentlyContinue)) { + $module.FailJson("Command '$executable' not found in $env:PATH.") +} + +$arguments = @() + +if ($include_volumes) { + foreach ($volume in $include_volumes) { + if ($volume.Length -eq 1) { + $arguments += "$($volume):" + } + else { + $arguments += $volume + } + } +} +else { + $arguments += "/C" +} + +if ($exclude_volumes) { + $arguments += "/E" + foreach ($volume in $exclude_volumes) { + if ($volume.Length -eq 1) { + $arguments += "$($volume):" + } + else { + $arguments += $volume + } + } +} + +if ($module.CheckMode) { + $arguments += "/A" +} +elseif ($freespace_consolidation) { + $arguments += "/X" +} + +if ($priority -eq "normal") { + $arguments += "/H" +} + +if ($parallel) { + $arguments += "/M" +} + +$arguments += "/V" + +$argument_string = Argv-ToString -arguments $arguments + +$start_datetime = [DateTime]::UtcNow +$module.Result.cmd = "$executable $argument_string" + +$command_result = Run-Command -command "$executable $argument_string" + +$end_datetime = [DateTime]::UtcNow + +$module.Result.stdout = $command_result.stdout +$module.Result.stderr = $command_result.stderr +$module.Result.rc = $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.Result.changed = $true + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_defrag.py b/ansible_collections/community/windows/plugins/modules/win_defrag.py new file mode 100644 index 000000000..7a268d50c --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_defrag.py @@ -0,0 +1,98 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: 2017, Dag Wieers (@dagwieers) <dag@wieers.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_defrag +short_description: Consolidate fragmented files on local volumes +description: +- Locates and consolidates fragmented files on local volumes to improve system performance. +- 'More information regarding C(win_defrag) is available from: U(https://technet.microsoft.com/en-us/library/cc731650%28v%3Dws.11.aspx%29)' +requirements: +- defrag.exe +options: + include_volumes: + description: + - A list of drive letters or mount point paths of the volumes to be defragmented. + - If this parameter is omitted, all volumes (not excluded) will be fragmented. + type: list + elements: str + exclude_volumes: + description: + - A list of drive letters or mount point paths to exclude from defragmentation. + type: list + elements: str + freespace_consolidation: + description: + - Perform free space consolidation on the specified volumes. + type: bool + default: no + priority: + description: + - Run the operation at low or normal priority. + type: str + choices: [ low, normal ] + default: low + parallel: + description: + - Run the operation on each volume in parallel in the background. + type: bool + default: no +author: +- Dag Wieers (@dagwieers) +''' + +EXAMPLES = r''' +- name: Defragment all local volumes (in parallel) + community.windows.win_defrag: + parallel: yes + +- name: 'Defragment all local volumes, except C: and D:' + community.windows.win_defrag: + exclude_volumes: [ C, D ] + +- name: 'Defragment volume D: with normal priority' + community.windows.win_defrag: + include_volumes: D + priority: normal + +- name: Consolidate free space (useful when reducing volumes) + community.windows.win_defrag: + freespace_consolidation: yes +''' + +RETURN = r''' +cmd: + description: The complete command line used by the module. + returned: always + type: str + sample: defrag.exe /C /V +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: +msg: + description: Possible error message on failure. + returned: failed + type: str + sample: Command 'defrag.exe' not found in $env:PATH. +changed: + description: Whether or not any changes were made. + returned: always + type: bool + sample: true +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_dhcp_lease.ps1 b/ansible_collections/community/windows/plugins/modules/win_dhcp_lease.ps1 new file mode 100644 index 000000000..004e16dca --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_dhcp_lease.ps1 @@ -0,0 +1,446 @@ +#!powershell + +# Copyright: (c) 2020 VMware, Inc. All Rights Reserved. +# SPDX-License-Identifier: GPL-3.0-only +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic + +$spec = @{ + options = @{ + type = @{ type = "str"; choices = "reservation", "lease"; default = "reservation" } + ip = @{ type = "str" } + scope_id = @{ type = "str" } + mac = @{ type = "str" } + duration = @{ type = "int" } + dns_hostname = @{ type = "str"; } + dns_regtype = @{ type = "str"; choices = "aptr", "a", "noreg"; default = "aptr" } + reservation_name = @{ type = "str"; } + description = @{ type = "str"; } + state = @{ type = "str"; choices = "absent", "present"; default = "present" } + } + required_if = @( + @("state", "present", @("mac", "ip"), $true), + @("state", "absent", @("mac", "ip"), $true) + ) + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) +$check_mode = $module.CheckMode + +$type = $module.Params.type +$ip = $module.Params.ip +$scope_id = $module.Params.scope_id +$mac = $module.Params.mac +$duration = $module.Params.duration +$dns_hostname = $module.Params.dns_hostname +$dns_regtype = $module.Params.dns_regtype +$reservation_name = $module.Params.reservation_name +$description = $module.Params.description +$state = $module.Params.state + +Function Convert-MacAddress { + Param( + [string]$mac + ) + + # Evaluate Length + if ($mac.Length -eq 12) { + # Insert Dashes + $mac = $mac.Insert(2, "-").Insert(5, "-").Insert(8, "-").Insert(11, "-").Insert(14, "-") + return $mac + } + elseif ($mac.Length -eq 17) { + # Replace Colons by Dashes + return ($mac -replace ':', '-') + } + else { + return $false + } +} + +Function Compare-DhcpLease { + Param( + [PSObject]$Original, + [PSObject]$Updated + ) + + # Compare values that we care about + -not ( + ($Original.AddressState -eq $Updated.AddressState) -and + ($Original.IPAddress -eq $Updated.IPAddress) -and + ($Original.ScopeId -eq $Updated.ScopeId) -and + ($Original.Name -eq $Updated.Name) -and + ($Original.Description -eq $Updated.Description) + ) +} + +Function Convert-ReturnValue { + Param( + $Object + ) + + return @{ + address_state = $Object.AddressState + client_id = $Object.ClientId + ip_address = $Object.IPAddress.IPAddressToString + scope_id = $Object.ScopeId.IPAddressToString + name = $Object.Name + description = $Object.Description + } +} + +# Parse Regtype +if ($dns_regtype) { + Switch ($dns_regtype) { + "aptr" { $dns_regtype = "AandPTR"; break } + "a" { $dns_regtype = "A"; break } + "noreg" { $dns_regtype = "NoRegistration"; break } + default { $dns_regtype = "NoRegistration"; break } + } +} + +Try { + # Import DHCP Server PS Module + Import-Module DhcpServer +} +Catch { + # Couldn't load the DhcpServer Module + $module.FailJson("The DhcpServer module failed to load properly: $($_.Exception.Message)", $_) +} + +# Find existing lease by MAC address +if ($mac) { + $mac = Convert-MacAddress -mac $mac + + if ($mac -eq $false) { + $module.FailJson("The MAC Address is not properly formatted") + } + else { + $current_lease = Get-DhcpServerv4Scope | Get-DhcpServerv4Lease | Where-Object ClientId -eq $mac + } +} + +# Find existing lease by IP address +if ($ip -and (-not $current_lease)) { + $current_lease = Get-DhcpServerv4Scope | Get-DhcpServerv4Lease | Where-Object IPAddress -eq $ip +} + +# Did we find a lease/reservation +if ($current_lease) { + $current_lease_exists = $true + $original_lease = $current_lease + $module.Diff.before = Convert-ReturnValue -Object $original_lease +} +else { + $current_lease_exists = $false +} + +# If we found a lease, is it a reservation? +if ($current_lease_exists -eq $true -and ($current_lease.AddressState -like "*Reservation*")) { + $current_lease_reservation = $true +} +else { + $current_lease_reservation = $false +} + +# State: Absent +# Ensure the DHCP Lease/Reservation is not present +if ($state -eq "absent") { + # If the lease doesn't exist, our work here is done + if ($current_lease_exists -eq $false) { + $module.Result.msg = "The lease doesn't exist." + } + else { + # If the lease exists, we need to destroy it + if ($current_lease_reservation -eq $true) { + # Try to remove reservation + Try { + $current_lease | Remove-DhcpServerv4Reservation -WhatIf:$check_mode + $state_absent_removed = $true + } + Catch { + $state_absent_removed = $false + $remove_err = $_ + } + } + else { + # Try to remove lease + Try { + $current_lease | Remove-DhcpServerv4Lease -WhatIf:$check_mode + $state_absent_removed = $true + } + Catch { + $state_absent_removed = $false + $remove_err = $_ + } + } + + # See if we removed the lease/reservation + if ($state_absent_removed) { + $module.Result.changed = $true + } + else { + $module.Result.lease = Convert-ReturnValue -Object $current_lease + $module.FailJson("Unable to remove lease/reservation: $($remove_err.Exception.Message)", $remove_err) + } + } +} + +# State: Present +# Ensure the DHCP Lease/Reservation is present, and consistent +if ($state -eq "present") { + # Current lease exists, and is not a reservation + if (($current_lease_reservation -eq $false) -and ($current_lease_exists -eq $true)) { + if ($type -eq "reservation") { + Try { + # Update parameters + $params = @{ } + + if ($mac) { + $params.ClientId = $mac + } + else { + $params.ClientId = $current_lease.ClientId + } + + if ($description) { + $params.Description = $description + } + else { + $params.Description = $current_lease.Description + } + + if ($reservation_name) { + $params.Name = $reservation_name + } + else { + $params.Name = "reservation-" + $params.ClientId + } + + # Desired type is reservation + $current_lease | Add-DhcpServerv4Reservation -WhatIf:$check_mode + + if (-not $check_mode) { + $current_reservation = Get-DhcpServerv4Lease -ClientId $params.ClientId -ScopeId $current_lease.ScopeId + } + + # Update the reservation with new values + $current_reservation | Set-DhcpServerv4Reservation @params -WhatIf:$check_mode + + if (-not $check_mode) { + $updated_reservation = Get-DhcpServerv4Lease -ClientId $params.ClientId -ScopeId $current_reservation.ScopeId + } + + if (-not $check_mode) { + # Compare Values + $module.Result.changed = Compare-DhcpLease -Original $original_lease -Updated $updated_reservation + $module.Result.lease = Convert-ReturnValue -Object $updated_reservation + } + else { + $module.Result.changed = $true + } + + $module.ExitJson() + } + Catch { + $module.FailJson("Could not convert lease to a reservation", $_) + } + } + } + + # Current lease exists, and is a reservation + if (($current_lease_reservation -eq $true) -and ($current_lease_exists -eq $true)) { + if ($type -eq "lease") { + Try { + # Desired type is a lease, remove the reservation + $current_lease | Remove-DhcpServerv4Reservation -WhatIf:$check_mode + + # Build a new lease object with remnants of the reservation + $lease_params = @{ + ClientId = $original_lease.ClientId + IPAddress = $original_lease.IPAddress.IPAddressToString + ScopeId = $original_lease.ScopeId.IPAddressToString + HostName = $original_lease.HostName + AddressState = 'Active' + } + + # Create new lease + Try { + Add-DhcpServerv4Lease @lease_params -WhatIf:$check_mode + } + Catch { + $module.FailJson("Unable to convert the reservation to a lease", $_) + } + + # Get the lease we just created + if (-not $check_mode) { + Try { + $new_lease = Get-DhcpServerv4Lease -ClientId $lease_params.ClientId -ScopeId $lease_params.ScopeId + } + Catch { + $module.FailJson("Unable to retreive the newly created lease", $_) + } + } + + if (-not $check_mode) { + $module.Result.lease = Convert-ReturnValue -Object $new_lease + } + + $module.Result.changed = $true + $module.ExitJson() + } + Catch { + $module.FailJson("Could not convert reservation to lease", $_) + } + } + + # Already in the desired state + if ($type -eq "reservation") { + + # Update parameters + $params = @{ } + + if ($mac) { + $params.ClientId = $mac + } + else { + $params.ClientId = $current_lease.ClientId + } + + if ($description) { + $params.Description = $description + } + else { + $params.Description = $current_lease.Description + } + + if ($reservation_name) { + $params.Name = $reservation_name + } + else { + # Original lease had a null name so let's generate one + if ($null -eq $original_lease.Name) { + $params.Name = "reservation-" + $original_lease.ClientId + } + else { + $params.Name = $original_lease.Name + } + } + + # Update the reservation with new values + $current_lease | Set-DhcpServerv4Reservation @params -WhatIf:$check_mode + + if (-not $check_mode) { + $reservation = Get-DhcpServerv4Lease -ClientId $current_lease.ClientId -ScopeId $current_lease.ScopeId + $module.Result.changed = Compare-DhcpLease -Original $original_lease -Updated $reservation + $module.Result.lease = Convert-ReturnValue -Object $reservation + } + else { + $module.Result.changed = $true + } + + # Return values + $module.ExitJson() + } + } + + # Lease Doesn't Exist - Create + if ($current_lease_exists -eq $false) { + # Required: Scope ID + if (-not $scope_id) { + $module.Result.changed = $false + $module.FailJson("The scope_id parameter is required for state=present when a lease or reservation doesn't already exist") + } + + # Required Parameters + $lease_params = @{ + ClientId = $mac + IPAddress = $ip + ScopeId = $scope_id + AddressState = 'Active' + Confirm = $false + } + + if ($duration) { + $lease_params.LeaseExpiryTime = (Get-Date).AddDays($duration) + } + + if ($dns_hostname) { + $lease_params.HostName = $dns_hostname + } + + if ($dns_regtype) { + $lease_params.DnsRR = $dns_regtype + } + + if ($description) { + $lease_params.Description = $description + } + + # Create Lease + Try { + # Create lease based on parameters + Add-DhcpServerv4Lease @lease_params -WhatIf:$check_mode + + # Retreive the lease + if (-not $check_mode) { + $new_lease = Get-DhcpServerv4Lease -ClientId $mac -ScopeId $scope_id + $module.Result.lease = Convert-ReturnValue -Object $new_lease + } + + # If lease is the desired type + if ($type -eq "lease") { + $module.Result.changed = $true + $module.ExitJson() + } + } + Catch { + # Failed to create lease + $module.FailJson("Could not create DHCP lease: $($_.Exception.Message)", $_) + } + + # Create Reservation + Try { + # If reservation is the desired type + if ($type -eq "reservation") { + if ($reservation_name) { + $lease_params.Name = $reservation_name + } + else { + $lease_params.Name = "reservation-" + $mac + } + + Try { + if ($check_mode) { + # In check mode, a lease won't exist for conversion, make one manually + Add-DhcpServerv4Reservation -ScopeId $scope_id -ClientId $mac -IPAddress $ip -WhatIf:$check_mode + } + else { + # Convert to Reservation + $new_lease | Add-DhcpServerv4Reservation -WhatIf:$check_mode + } + } + Catch { + # Failed to create reservation + $module.FailJson("Could not create DHCP reservation: $($_.Exception.Message)", $_) + } + + if (-not $check_mode) { + # Get DHCP reservation object + $new_lease = Get-DhcpServerv4Reservation -ClientId $mac -ScopeId $scope_id + $module.Result.lease = Convert-ReturnValue -Object $new_lease + } + + $module.Result.changed = $true + } + } + Catch { + # Failed to create reservation + $module.FailJson("Could not create DHCP reservation: $($_.Exception.Message)", $_) + } + } +} + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_dhcp_lease.py b/ansible_collections/community/windows/plugins/modules/win_dhcp_lease.py new file mode 100644 index 000000000..d70bcf2f3 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_dhcp_lease.py @@ -0,0 +1,135 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020 VMware, Inc. All Rights Reserved. +# SPDX-License-Identifier: GPL-3.0-only +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_dhcp_lease +short_description: Manage Windows Server DHCP Leases +author: Joe Zollo (@joezollo) +requirements: + - This module requires Windows Server 2012 or Newer +description: + - Manage Windows Server DHCP Leases (IPv4 Only) + - Adds, Removes and Modifies DHCP Leases and Reservations + - Task should be delegated to a Windows DHCP Server +options: + type: + description: + - The type of DHCP address. + - Leases expire as defined by l(duration). + - When l(duration) is not specified, the server default is used. + - Reservations are permanent. + type: str + default: reservation + choices: [ reservation, lease ] + state: + description: + - Specifies the desired state of the DHCP lease or reservation. + type: str + default: present + choices: [ present, absent ] + ip: + description: + - The IPv4 address of the client server/computer. + - This is a required parameter, if l(mac) is not set. + - Can be used to identify an existing lease/reservation, instead of l(mac). + type: str + required: no + scope_id: + description: + - Specifies the scope identifier as defined by the DHCP server. + - This is a required parameter, if l(state=present) and the reservation or lease + doesn't already exist. Not required if updating an existing lease or reservation. + type: str + mac: + description: + - Specifies the client identifier to be set on the IPv4 address. + - This is a required parameter, if l(ip) is not set. + - Windows clients use the MAC address as the client ID. + - Linux and other operating systems can use other types of identifiers. + - Can be used to identify an existing lease/reservation, instead of l(ip). + type: str + duration: + description: + - Specifies the duration of the DHCP lease in days. + - The duration value only applies to l(type=lease). + - Defaults to the duration specified by the DHCP server + configuration. + - Only applicable to l(type=lease). + type: int + dns_hostname: + description: + - Specifies the DNS hostname of the client for which the IP address + lease is to be added. + type: str + dns_regtype: + description: + - Indicates the type of DNS record to be registered by the DHCP. + server service for this lease. + - l(a) results in an A record being registered. + - l(aptr) results in both A and PTR records to be registered. + - l(noreg) results in no DNS records being registered. + type: str + default: aptr + choices: [ aptr, a, noreg ] + reservation_name: + description: + - Specifies the name of the reservation being created. + - Only applicable to l(type=reservation). + type: str + description: + description: + - Specifies the description for reservation being created. + - Only applicable to l(type=reservation). + type: str +''' + +EXAMPLES = r''' +- name: Ensure DHCP reservation exists + community.windows.win_dhcp_lease: + type: reservation + ip: 192.168.100.205 + scope_id: 192.168.100.0 + mac: 00:B1:8A:D1:5A:1F + dns_hostname: "{{ ansible_inventory }}" + description: Testing Server + +- name: Ensure DHCP lease or reservation does not exist + community.windows.win_dhcp_lease: + mac: 00:B1:8A:D1:5A:1F + state: absent + +- name: Ensure DHCP lease or reservation does not exist + community.windows.win_dhcp_lease: + ip: 192.168.100.205 + state: absent + +- name: Convert DHCP lease to reservation & update description + community.windows.win_dhcp_lease: + type: reservation + ip: 192.168.100.205 + description: Testing Server + +- name: Convert DHCP reservation to lease + community.windows.win_dhcp_lease: + type: lease + ip: 192.168.100.205 +''' + +RETURN = r''' +lease: + description: New/Updated DHCP object parameters + returned: When l(state=present) + type: dict + sample: + address_state: InactiveReservation + client_id: 0a-0b-0c-04-05-aa + description: Really Fancy + ip_address: 172.16.98.230 + name: null + scope_id: 172.16.98.0 +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_disk_facts.ps1 b/ansible_collections/community/windows/plugins/modules/win_disk_facts.ps1 new file mode 100644 index 000000000..2c2cecb5e --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_disk_facts.ps1 @@ -0,0 +1,280 @@ +#!powershell + +# Copyright: (c) 2017, Marc Tschapek <marc.tschapek@itelligence.de> +# 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 + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version 2.0 + +$spec = @{ + options = @{ + filter = @{ + type = "list" + elements = "str" + choices = "physical_disk", "virtual_disk", "win32_disk_drive", "partitions", "volumes" + default = "physical_disk", "virtual_disk", "win32_disk_drive", "partitions", "volumes" + } + } + supports_check_mode = $true +} +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +# Functions +function Test-Admin { + $CurrentUser = New-Object Security.Principal.WindowsPrincipal $([Security.Principal.WindowsIdentity]::GetCurrent()) + $IsAdmin = $CurrentUser.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) + return $IsAdmin +} + +# Check admin rights +if (-not (Test-Admin)) { + $module.FailJson("Module was not started with elevated rights") +} + + +# Create a new result object +$module.Result.changed = $false +$module.Result.ansible_facts = @{ ansible_disks = @() } + +# Search disks +try { + $disks = Get-Disk +} +catch { + $module.FailJson("Failed to search the disks on the target: $($_.Exception.Message)", $_) +} + +foreach ($disk in $disks) { + $disk_info = @{} + if ("physical_disk" -in $module.Params.filter) { + + $pdisk = Get-PhysicalDisk -ErrorAction SilentlyContinue | Where-Object { + $_.DeviceId -eq $disk.Number + + } + if ($pdisk) { + $disk_info["physical_disk"] += @{ + size = $pdisk.Size + allocated_size = $pdisk.AllocatedSize + device_id = $pdisk.DeviceId + friendly_name = $pdisk.FriendlyName + operational_status = $pdisk.OperationalStatus + health_status = $pdisk.HealthStatus + bus_type = $pdisk.BusType + usage_type = $pdisk.Usage + supported_usages = $pdisk.SupportedUsages + spindle_speed = $pdisk.SpindleSpeed + firmware_version = $pdisk.FirmwareVersion + physical_location = $pdisk.PhysicalLocation + manufacturer = $pdisk.Manufacturer + model = $pdisk.Model + can_pool = $pdisk.CanPool + indication_enabled = $pdisk.IsIndicationEnabled + partial = $pdisk.IsPartial + serial_number = $pdisk.SerialNumber + object_id = $pdisk.ObjectId + unique_id = $pdisk.UniqueId + } + if ([single]"$([System.Environment]::OSVersion.Version.Major).$([System.Environment]::OSVersion.Version.Minor)" -ge 6.3) { + $disk_info.physical_disk.media_type = $pdisk.MediaType + } + if (-not $pdisk.CanPool) { + $disk_info.physical_disk.cannot_pool_reason = $pdisk.CannotPoolReason + } + if ("virtual_disk" -in $module.Params.filter) { + $vdisk = Get-VirtualDisk -PhysicalDisk $pdisk -ErrorAction SilentlyContinue + if ($vdisk) { + $disk_info["virtual_disk"] += @{ + size = $vdisk.Size + allocated_size = $vdisk.AllocatedSize + footprint_on_pool = $vdisk.FootprintOnPool + name = $vdisk.name + friendly_name = $vdisk.FriendlyName + operational_status = $vdisk.OperationalStatus + health_status = $vdisk.HealthStatus + provisioning_type = $vdisk.ProvisioningType + allocation_unit_size = $vdisk.AllocationUnitSize + media_type = $vdisk.MediaType + parity_layout = $vdisk.ParityLayout + access = $vdisk.Access + detached_reason = $vdisk.DetachedReason + write_cache_size = $vdisk.WriteCacheSize + fault_domain_awareness = $vdisk.FaultDomainAwareness + inter_leave = $vdisk.InterLeave + deduplication_enabled = $vdisk.IsDeduplicationEnabled + enclosure_aware = $vdisk.IsEnclosureAware + manual_attach = $vdisk.IsManualAttach + snapshot = $vdisk.IsSnapshot + tiered = $vdisk.IsTiered + physical_sector_size = $vdisk.PhysicalSectorSize + logical_sector_size = $vdisk.LogicalSectorSize + available_copies = $vdisk.NumberOfAvailableCopies + columns = $vdisk.NumberOfColumns + groups = $vdisk.NumberOfGroups + physical_disk_redundancy = $vdisk.PhysicalDiskRedundancy + read_cache_size = $vdisk.ReadCacheSize + request_no_spof = $vdisk.RequestNoSinglePointOfFailure + resiliency_setting_name = $vdisk.ResiliencySettingName + object_id = $vdisk.ObjectId + unique_id_format = $vdisk.UniqueIdFormat + unique_id = $vdisk.UniqueId + } + } + } + } + } + if ("win32_disk_drive" -in $module.Params.filter) { + $win32_disk_drive = Get-CimInstance -ClassName Win32_DiskDrive -ErrorAction SilentlyContinue | Where-Object { + if ($_.SerialNumber) { + $_.SerialNumber -eq $disk.SerialNumber + } + elseif ($disk.UniqueIdFormat -eq 'Vendor Specific') { + $_.PNPDeviceID -eq $disk.UniqueId.split(':')[0] + } + } + if ($win32_disk_drive) { + $disk_info["win32_disk_drive"] += @{ + availability = $win32_disk_drive.Availability + bytes_per_sector = $win32_disk_drive.BytesPerSector + capabilities = $win32_disk_drive.Capabilities + capability_descriptions = $win32_disk_drive.CapabilityDescriptions + caption = $win32_disk_drive.Caption + compression_method = $win32_disk_drive.CompressionMethod + config_manager_error_code = $win32_disk_drive.ConfigManagerErrorCode + config_manager_user_config = $win32_disk_drive.ConfigManagerUserConfig + creation_class_name = $win32_disk_drive.CreationClassName + default_block_size = $win32_disk_drive.DefaultBlockSize + description = $win32_disk_drive.Description + device_id = $win32_disk_drive.DeviceID + error_cleared = $win32_disk_drive.ErrorCleared + error_description = $win32_disk_drive.ErrorDescription + error_methodology = $win32_disk_drive.ErrorMethodology + firmware_revision = $win32_disk_drive.FirmwareRevision + index = $win32_disk_drive.Index + install_date = $win32_disk_drive.InstallDate + interface_type = $win32_disk_drive.InterfaceType + last_error_code = $win32_disk_drive.LastErrorCode + manufacturer = $win32_disk_drive.Manufacturer + max_block_size = $win32_disk_drive.MaxBlockSize + max_media_size = $win32_disk_drive.MaxMediaSize + media_loaded = $win32_disk_drive.MediaLoaded + media_type = $win32_disk_drive.MediaType + min_block_size = $win32_disk_drive.MinBlockSize + model = $win32_disk_drive.Model + name = $win32_disk_drive.Name + needs_cleaning = $win32_disk_drive.NeedsCleaning + number_of_media_supported = $win32_disk_drive.NumberOfMediaSupported + partitions = $win32_disk_drive.Partitions + pnp_device_id = $win32_disk_drive.PNPDeviceID + power_management_capabilities = $win32_disk_drive.PowerManagementCapabilities + power_management_supported = $win32_disk_drive.PowerManagementSupported + scsi_bus = $win32_disk_drive.SCSIBus + scsi_logical_unit = $win32_disk_drive.SCSILogicalUnit + scsi_port = $win32_disk_drive.SCSIPort + scsi_target_id = $win32_disk_drive.SCSITargetId + sectors_per_track = $win32_disk_drive.SectorsPerTrack + serial_number = $win32_disk_drive.SerialNumber + signature = $win32_disk_drive.Signature + size = $win32_disk_drive.Size + status = $win32_disk_drive.status + status_info = $win32_disk_drive.StatusInfo + system_creation_class_name = $win32_disk_drive.SystemCreationClassName + system_name = $win32_disk_drive.SystemName + total_cylinders = $win32_disk_drive.TotalCylinders + total_heads = $win32_disk_drive.TotalHeads + total_sectors = $win32_disk_drive.TotalSectors + total_tracks = $win32_disk_drive.TotalTracks + tracks_per_cylinder = $win32_disk_drive.TracksPerCylinder + } + } + } + $disk_info.number = $disk.Number + $disk_info.size = $disk.Size + $disk_info.bus_type = $disk.BusType + $disk_info.friendly_name = $disk.FriendlyName + $disk_info.partition_style = $disk.PartitionStyle + $disk_info.partition_count = $disk.NumberOfPartitions + $disk_info.operational_status = $disk.OperationalStatus + $disk_info.sector_size = $disk.PhysicalSectorSize + $disk_info.read_only = $disk.IsReadOnly + $disk_info.bootable = $disk.IsBoot + $disk_info.system_disk = $disk.IsSystem + $disk_info.clustered = $disk.IsClustered + $disk_info.manufacturer = $disk.Manufacturer + $disk_info.model = $disk.Model + $disk_info.firmware_version = $disk.FirmwareVersion + $disk_info.location = $disk.Location + $disk_info.serial_number = $disk.SerialNumber + $disk_info.unique_id = $disk.UniqueId + $disk_info.guid = $disk.Guid + $disk_info.path = $disk.Path + if (("partitions" -in $module.Params.filter -or "volumes" -in $module.Params.filter) -and ($null -ne $disk.Number)) { + $parts = Get-Partition -DiskNumber $($disk.Number) -ErrorAction SilentlyContinue + if ($parts) { + $disk_info["partitions"] += @() + foreach ($part in $parts) { + $partition_info = @{ + number = $part.PartitionNumber + size = $part.Size + type = $part.Type + drive_letter = $part.DriveLetter + transition_state = $part.TransitionState + offset = $part.Offset + hidden = $part.IsHidden + shadow_copy = $part.IsShadowCopy + guid = $part.Guid + access_paths = $part.AccessPaths + } + if ($disks.PartitionStyle -eq "GPT") { + $partition_info.gpt_type = $part.GptType + $partition_info.no_default_driveletter = $part.NoDefaultDriveLetter + } + elseif ($disks.PartitionStyle -eq "MBR") { + $partition_info.mbr_type = $part.MbrType + $partition_info.active = $part.IsActive + } + if ("volumes" -in $module.Params.filter) { + $vols = Get-Volume -Partition $part -ErrorAction SilentlyContinue + if ($vols) { + $partition_info["volumes"] += @() + foreach ($vol in $vols) { + $volume_info = @{ + size = $vol.Size + size_remaining = $vol.SizeRemaining + type = $vol.FileSystem + label = $vol.FileSystemLabel + health_status = $vol.HealthStatus + drive_type = $vol.DriveType + object_id = $vol.ObjectId + path = $vol.Path + } + if ([System.Environment]::OSVersion.Version.Major -ge 10) { + $volume_info.allocation_unit_size = $vol.AllocationUnitSize + } + else { + $volPath = ($vol.Path.TrimStart("\\?\")).TrimEnd("\") + $BlockSize = ( + Get-CimInstance ` + -Query "SELECT BlockSize FROM Win32_Volume WHERE DeviceID like '%$volPath%'" ` + -ErrorAction SilentlyContinue | Select-Object BlockSize).BlockSize + $volume_info.allocation_unit_size = $BlockSize + } + $partition_info.volumes += $volume_info + } + } + } + $disk_info.partitions += $partition_info + } + } + } + $module.Result.ansible_facts.ansible_disks += $disk_info +} + +# Sort by disk number property +$module.Result.ansible_facts.ansible_disks = @() + ($module.Result.ansible_facts.ansible_disks | Sort-Object -Property { $_.Number }) + +# Return result +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_disk_facts.py b/ansible_collections/community/windows/plugins/modules/win_disk_facts.py new file mode 100644 index 000000000..a303e5713 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_disk_facts.py @@ -0,0 +1,902 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2017, Marc Tschapek <marc.tschapek@itelligence.de> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_disk_facts +short_description: Show the attached disks and disk information of the target host +description: + - With the module you can retrieve and output detailed information about the attached disks of the target and + its volumes and partitions if existent. +requirements: + - Windows 8.1 / Windows 2012 (NT 6.2) +notes: + - In order to understand all the returned properties and values please visit the following site and open the respective MSFT class + U(https://msdn.microsoft.com/en-us/library/windows/desktop/hh830612.aspx) +author: + - Marc Tschapek (@marqelme) +options: + filter: + description: + - Allows to filter returned facts by type of disk information. + - If volumes are selected partitions will be returned as well. + type: list + elements: str + choices: [ physical_disk, virtual_disk, win32_disk_drive, partitions, volumes ] + default: [ physical_disk, virtual_disk, win32_disk_drive, partitions, volumes ] + version_added: 1.9.0 +''' + +EXAMPLES = r''' +- name: Get disk facts + community.windows.win_disk_facts: + +- name: Output first disk size + debug: + var: ansible_facts.disks[0].size + +- name: Convert first system disk into various formats + debug: + msg: '{{ disksize_gib }} vs {{ disksize_gib_human }}' + vars: + # Get first system disk + disk: '{{ ansible_facts.disks|selectattr("system_disk")|first }}' + + # Show disk size in Gibibytes + disksize_gib_human: '{{ disk.size|filesizeformat(true) }}' # returns "223.6 GiB" (human readable) + disksize_gib: '{{ (disk.size/1024|pow(3))|round|int }} GiB' # returns "224 GiB" (value in GiB) + + # Show disk size in Gigabytes + disksize_gb_human: '{{ disk.size|filesizeformat }}' # returns "240.1 GB" (human readable) + disksize_gb: '{{ (disk.size/1000|pow(3))|round|int }} GB' # returns "240 GB" (value in GB) + +- name: Output second disk serial number + debug: + var: ansible_facts.disks[1].serial_number + +- name: get disk physical_disk and partition facts on the target + win_disk_facts: + filter: + - physical_disk + - partitions +''' + +RETURN = r''' +ansible_facts: + description: Dictionary containing all the detailed information about the disks of the target. + returned: always + type: complex + contains: + ansible_disks: + description: Detailed information about one particular disk. + returned: if disks were found + type: list + contains: + number: + description: Disk number of the particular disk. + returned: always + type: int + sample: 0 + size: + description: Size in bytes of the particular disk. + returned: always + type: int + sample: 227727638528 + bus_type: + description: Bus type of the particular disk. + returned: always + type: str + sample: "SCSI" + friendly_name: + description: Friendly name of the particular disk. + returned: always + type: str + sample: "Red Hat VirtIO SCSI Disk Device" + partition_style: + description: Partition style of the particular disk. + returned: always + type: str + sample: "MBR" + partition_count: + description: Number of partitions on the particular disk. + returned: always + type: int + sample: 4 + operational_status: + description: Operational status of the particular disk. + returned: always + type: str + sample: "Online" + sector_size: + description: Sector size in bytes of the particular disk. + returned: always + type: int + sample: 4096 + read_only: + description: Read only status of the particular disk. + returned: always + type: bool + sample: true + bootable: + description: Information whether the particular disk is a bootable disk. + returned: always + type: bool + sample: false + system_disk: + description: Information whether the particular disk is a system disk. + returned: always + type: bool + sample: true + clustered: + description: Information whether the particular disk is clustered (part of a failover cluster). + returned: always + type: bool + sample: false + manufacturer: + description: Manufacturer of the particular disk. + returned: always + type: str + sample: "Red Hat" + model: + description: Model specification of the particular disk. + returned: always + type: str + sample: "VirtIO" + firmware_version: + description: Firmware version of the particular disk. + returned: always + type: str + sample: "0001" + location: + description: Location of the particular disk on the target. + returned: always + type: str + sample: "PCIROOT(0)#PCI(0400)#SCSI(P00T00L00)" + serial_number: + description: Serial number of the particular disk on the target. + returned: always + type: str + sample: "b62beac80c3645e5877f" + unique_id: + description: Unique ID of the particular disk on the target. + returned: always + type: str + sample: "3141463431303031" + guid: + description: GUID of the particular disk on the target. + returned: if existent + type: str + sample: "{efa5f928-57b9-47fc-ae3e-902e85fbe77f}" + path: + description: Path of the particular disk on the target. + returned: always + type: str + sample: "\\\\?\\scsi#disk&ven_red_hat&prod_virtio#4&23208fd0&1&000000#{<id>}" + partitions: + description: Detailed information about one particular partition on the specified disk. + returned: if existent + type: list + contains: + number: + description: Number of the particular partition. + returned: always + type: int + sample: 1 + size: + description: + - Size in bytes of the particular partition. + returned: always + type: int + sample: 838860800 + type: + description: Type of the particular partition. + returned: always + type: str + sample: "IFS" + gpt_type: + description: gpt type of the particular partition. + returned: if partition_style property of the particular disk has value "GPT" + type: str + sample: "{e3c9e316-0b5c-4db8-817d-f92df00215ae}" + no_default_driveletter: + description: Information whether the particular partition has a default drive letter or not. + returned: if partition_style property of the particular disk has value "GPT" + type: bool + sample: true + mbr_type: + description: mbr type of the particular partition. + returned: if partition_style property of the particular disk has value "MBR" + type: int + sample: 7 + active: + description: Information whether the particular partition is an active partition or not. + returned: if partition_style property of the particular disk has value "MBR" + type: bool + sample: true + drive_letter: + description: Drive letter of the particular partition. + returned: if existent + type: str + sample: "C" + transition_state: + description: Transition state of the particular partition. + returned: always + type: int + sample: 1 + offset: + description: Offset of the particular partition. + returned: always + type: int + sample: 368050176 + hidden: + description: Information whether the particular partition is hidden or not. + returned: always + type: bool + sample: true + shadow_copy: + description: Information whether the particular partition is a shadow copy of another partition. + returned: always + type: bool + sample: false + guid: + description: GUID of the particular partition. + returned: if existent + type: str + sample: "{302e475c-6e64-4674-a8e2-2f1c7018bf97}" + access_paths: + description: Access paths of the particular partition. + returned: if existent + type: str + sample: "\\\\?\\Volume{85bdc4a8-f8eb-11e6-80fa-806e6f6e6963}\\" + volumes: + description: Detailed information about one particular volume on the specified partition. + returned: if existent + type: list + contains: + size: + description: + - Size in bytes of the particular volume. + returned: always + type: int + sample: 838856704 + size_remaining: + description: + - Remaining size in bytes of the particular volume. + returned: always + type: int + sample: 395620352 + type: + description: File system type of the particular volume. + returned: always + type: str + sample: "NTFS" + label: + description: File system label of the particular volume. + returned: always + type: str + sample: "System Reserved" + health_status: + description: Health status of the particular volume. + returned: always + type: str + sample: "Healthy" + drive_type: + description: Drive type of the particular volume. + returned: always + type: str + sample: "Fixed" + allocation_unit_size: + description: Allocation unit size in bytes of the particular volume. + returned: always + type: int + sample: 4096 + object_id: + description: Object ID of the particular volume. + returned: always + type: str + sample: "\\\\?\\Volume{85bdc4a9-f8eb-11e6-80fa-806e6f6e6963}\\" + path: + description: Path of the particular volume. + returned: always + type: str + sample: "\\\\?\\Volume{85bdc4a9-f8eb-11e6-80fa-806e6f6e6963}\\" + physical_disk: + description: Detailed information about physical disk properties of the particular disk. + returned: if existent + type: complex + contains: + media_type: + description: Media type of the particular physical disk. + returned: always + type: str + sample: "UnSpecified" + size: + description: + - Size in bytes of the particular physical disk. + returned: always + type: int + sample: 240057409536 + allocated_size: + description: + - Allocated size in bytes of the particular physical disk. + returned: always + type: int + sample: 240057409536 + device_id: + description: Device ID of the particular physical disk. + returned: always + type: str + sample: "0" + friendly_name: + description: Friendly name of the particular physical disk. + returned: always + type: str + sample: "PhysicalDisk0" + operational_status: + description: Operational status of the particular physical disk. + returned: always + type: str + sample: "OK" + health_status: + description: Health status of the particular physical disk. + returned: always + type: str + sample: "Healthy" + bus_type: + description: Bus type of the particular physical disk. + returned: always + type: str + sample: "SCSI" + usage_type: + description: Usage type of the particular physical disk. + returned: always + type: str + sample: "Auto-Select" + supported_usages: + description: Supported usage types of the particular physical disk. + returned: always + type: complex + contains: + Count: + description: Count of supported usage types. + returned: always + type: int + sample: 5 + value: + description: List of supported usage types. + returned: always + type: str + sample: "Auto-Select, Hot Spare" + spindle_speed: + description: Spindle speed in rpm of the particular physical disk. + returned: always + type: int + sample: 4294967295 + physical_location: + description: Physical location of the particular physical disk. + returned: always + type: str + sample: "Integrated : Adapter 3 : Port 0 : Target 0 : LUN 0" + manufacturer: + description: Manufacturer of the particular physical disk. + returned: always + type: str + sample: "SUSE" + model: + description: Model of the particular physical disk. + returned: always + type: str + sample: "Xen Block" + can_pool: + description: Information whether the particular physical disk can be added to a storage pool. + returned: always + type: bool + sample: false + cannot_pool_reason: + description: Information why the particular physical disk can not be added to a storage pool. + returned: if can_pool property has value false + type: str + sample: "Insufficient Capacity" + indication_enabled: + description: Information whether indication is enabled for the particular physical disk. + returned: always + type: bool + sample: true + partial: + description: Information whether the particular physical disk is partial. + returned: always + type: bool + sample: false + serial_number: + description: Serial number of the particular physical disk. + returned: always + type: str + sample: "b62beac80c3645e5877f" + object_id: + description: Object ID of the particular physical disk. + returned: always + type: str + sample: '{1}\\\\HOST\\root/Microsoft/Windows/Storage/Providers_v2\\SPACES_PhysicalDisk.ObjectId=\"{<object_id>}:PD:{<pd>}\"' + unique_id: + description: Unique ID of the particular physical disk. + returned: always + type: str + sample: "3141463431303031" + virtual_disk: + description: Detailed information about virtual disk properties of the particular disk. + returned: if existent + type: complex + contains: + size: + description: + - Size in bytes of the particular virtual disk. + returned: always + type: int + sample: 240057409536 + allocated_size: + description: + - Allocated size in bytes of the particular virtual disk. + returned: always + type: int + sample: 240057409536 + footprint_on_pool: + description: + - Footprint on pool in bytes of the particular virtual disk. + returned: always + type: int + sample: 240057409536 + name: + description: Name of the particular virtual disk. + returned: always + type: str + sample: "vDisk1" + friendly_name: + description: Friendly name of the particular virtual disk. + returned: always + type: str + sample: "Prod2 Virtual Disk" + operational_status: + description: Operational status of the particular virtual disk. + returned: always + type: str + sample: "OK" + health_status: + description: Health status of the particular virtual disk. + returned: always + type: str + sample: "Healthy" + provisioning_type: + description: Provisioning type of the particular virtual disk. + returned: always + type: str + sample: "Thin" + allocation_unit_size: + description: Allocation unit size in bytes of the particular virtual disk. + returned: always + type: int + sample: 4096 + media_type: + description: Media type of the particular virtual disk. + returned: always + type: str + sample: "Unspecified" + parity_layout: + description: Parity layout of the particular virtual disk. + returned: if existent + type: int + sample: 1 + access: + description: Access of the particular virtual disk. + returned: always + type: str + sample: "Read/Write" + detached_reason: + description: Detached reason of the particular virtual disk. + returned: always + type: str + sample: "None" + write_cache_size: + description: Write cache size in byte of the particular virtual disk. + returned: always + type: int + sample: 100 + fault_domain_awareness: + description: Fault domain awareness of the particular virtual disk. + returned: always + type: str + sample: "PhysicalDisk" + inter_leave: + description: + - Inter leave in bytes of the particular virtual disk. + returned: always + type: int + sample: 102400 + deduplication_enabled: + description: Information whether deduplication is enabled for the particular virtual disk. + returned: always + type: bool + sample: true + enclosure_aware: + description: Information whether the particular virtual disk is enclosure aware. + returned: always + type: bool + sample: false + manual_attach: + description: Information whether the particular virtual disk is manual attached. + returned: always + type: bool + sample: true + snapshot: + description: Information whether the particular virtual disk is a snapshot. + returned: always + type: bool + sample: false + tiered: + description: Information whether the particular virtual disk is tiered. + returned: always + type: bool + sample: true + physical_sector_size: + description: Physical sector size in bytes of the particular virtual disk. + returned: always + type: int + sample: 4096 + logical_sector_size: + description: Logical sector size in byte of the particular virtual disk. + returned: always + type: int + sample: 512 + available_copies: + description: Number of the available copies of the particular virtual disk. + returned: if existent + type: int + sample: 1 + columns: + description: Number of the columns of the particular virtual disk. + returned: always + type: int + sample: 2 + groups: + description: Number of the groups of the particular virtual disk. + returned: always + type: int + sample: 1 + physical_disk_redundancy: + description: Type of the physical disk redundancy of the particular virtual disk. + returned: always + type: int + sample: 1 + read_cache_size: + description: Read cache size in byte of the particular virtual disk. + returned: always + type: int + sample: 0 + request_no_spof: + description: Information whether the particular virtual disk requests no single point of failure. + returned: always + type: bool + sample: true + resiliency_setting_name: + description: Type of the physical disk redundancy of the particular virtual disk. + returned: always + type: int + sample: 1 + object_id: + description: Object ID of the particular virtual disk. + returned: always + type: str + sample: '{1}\\\\HOST\\root/Microsoft/Windows/Storage/Providers_v2\\SPACES_VirtualDisk.ObjectId=\"{<object_id>}:VD:{<vd>}\"' + unique_id: + description: Unique ID of the particular virtual disk. + returned: always + type: str + sample: "260542E4C6B01D47A8FA7630FD90FFDE" + unique_id_format: + description: Unique ID format of the particular virtual disk. + returned: always + type: str + sample: "Vendor Specific" + win32_disk_drive: + description: Representation of the Win32_DiskDrive class. + returned: if existent + type: complex + contains: + availability: + description: Availability and status of the device. + returned: always + type: int + bytes_per_sector: + description: Number of bytes in each sector for the physical disk drive. + returned: always + type: int + sample: 512 + capabilities: + description: + - Array of capabilities of the media access device. + - For example, the device may support random access (3), removable media (7), and automatic cleaning (9). + returned: always + type: list + sample: + - 3 + - 4 + capability_descriptions: + description: + - List of more detailed explanations for any of the access device features indicated in the Capabilities array. + - Note, each entry of this array is related to the entry in the Capabilities array that is located at the same index. + returned: always + type: list + sample: + - Random Access + - Supports Writing + caption: + description: Short description of the object. + returned: always + type: str + sample: VMware Virtual disk SCSI Disk Device + compression_method: + description: Algorithm or tool used by the device to support compression. + returned: always + type: str + sample: Compressed + config_manager_error_code: + description: Windows Configuration Manager error code. + returned: always + type: int + sample: 0 + config_manager_user_config: + description: If True, the device is using a user-defined configuration. + returned: always + type: bool + sample: true + creation_class_name: + description: + - Name of the first concrete class to appear in the inheritance chain used in the creation of an instance. + - When used with the other key properties of the class, the property allows all instances of this class + - and its subclasses to be uniquely identified. + returned: always + type: str + sample: Win32_DiskDrive + default_block_size: + description: Default block size, in bytes, for this device. + returned: always + type: int + sample: 512 + description: + description: Description of the object. + returned: always + type: str + sample: Disk drive + device_id: + description: Unique identifier of the disk drive with other devices on the system. + returned: always + type: str + sample: "\\\\.\\PHYSICALDRIVE0" + error_cleared: + description: If True, the error reported in LastErrorCode is now cleared. + returned: always + type: bool + sample: true + error_description: + description: + - More information about the error recorded in LastErrorCode, + - and information on any corrective actions that may be taken. + returned: always + type: str + error_methodology: + description: Type of error detection and correction supported by this device. + returned: always + type: str + firmware_revision: + description: Revision for the disk drive firmware that is assigned by the manufacturer. + returned: always + type: str + sample: 1.0 + index: + description: + - Physical drive number of the given drive. + - This property is filled by the STORAGE_DEVICE_NUMBER structure returned from the IOCTL_STORAGE_GET_DEVICE_NUMBER control code + - A value of 0xffffffff indicates that the given drive does not map to a physical drive. + returned: always + type: int + sample: 0 + install_date: + description: Date and time the object was installed. This property does not need a value to indicate that the object is installed. + returned: always + type: str + interface_type: + description: Interface type of physical disk drive. + returned: always + type: str + sample: SCSI + last_error_code: + description: Last error code reported by the logical device. + returned: always + type: int + manufacturer: + description: Name of the disk drive manufacturer. + returned: always + type: str + sample: Seagate + max_block_size: + description: Maximum block size, in bytes, for media accessed by this device. + returned: always + type: int + max_media_size: + description: Maximum media size, in kilobytes, of media supported by this device. + returned: always + type: int + media_loaded: + description: + - If True, the media for a disk drive is loaded, which means that the device has a readable file system and is accessible. + - For fixed disk drives, this property will always be TRUE. + returned: always + type: bool + sample: true + media_type: + description: Type of media used or accessed by this device. + returned: always + type: str + sample: Fixed hard disk media + min_block_size: + description: Minimum block size, in bytes, for media accessed by this device. + returned: always + type: int + model: + description: Manufacturer's model number of the disk drive. + returned: always + type: str + sample: ST32171W + name: + description: Label by which the object is known. When subclassed, the property can be overridden to be a key property. + returned: always + type: str + sample: \\\\.\\PHYSICALDRIVE0 + needs_cleaning: + description: + - If True, the media access device needs cleaning. + - Whether manual or automatic cleaning is possible is indicated in the Capabilities property. + returned: always + type: bool + number_of_media_supported: + description: + - Maximum number of media which can be supported or inserted + - (when the media access device supports multiple individual media). + returned: always + type: int + partitions: + description: Number of partitions on this physical disk drive that are recognized by the operating system. + returned: always + type: int + sample: 3 + pnp_device_id: + description: Windows Plug and Play device identifier of the logical device. + returned: always + type: str + sample: "SCSI\\DISK&VEN_VMWARE&PROD_VIRTUAL_DISK\\5&1982005&0&000000" + power_management_capabilities: + description: Array of the specific power-related capabilities of a logical device. + returned: always + type: list + power_management_supported: + description: + - If True, the device can be power-managed (can be put into suspend mode, and so on). + - The property does not indicate that power management features are currently enabled, + - only that the logical device is capable of power management. + returned: always + type: bool + scsi_bus: + description: SCSI bus number of the disk drive. + returned: always + type: int + sample: 0 + scsi_logical_unit: + description: SCSI logical unit number (LUN) of the disk drive. + returned: always + type: int + sample: 0 + scsi_port: + description: SCSI port number of the disk drive. + returned: always + type: int + sample: 0 + scsi_target_id: + description: SCSI identifier number of the disk drive. + returned: always + type: int + sample: 0 + sectors_per_track: + description: Number of sectors in each track for this physical disk drive. + returned: always + type: int + sample: 63 + serial_number: + description: Number allocated by the manufacturer to identify the physical media. + returned: always + type: str + sample: 6000c298f34101b38cb2b2508926b9de + signature: + description: Disk identification. This property can be used to identify a shared resource. + returned: always + type: int + size: + description: + - Size of the disk drive. It is calculated by multiplying the total number of cylinders, tracks in each cylinder, + - sectors in each track, and bytes in each sector. + returned: always + type: int + sample: 53686402560 + status: + description: + - Current status of the object. Various operational and nonoperational statuses can be defined. + - 'Operational statuses include: "OK", "Degraded", and "Pred Fail"' + - (an element, such as a SMART-enabled hard disk drive, may be functioning properly but predicting a failure in the near future). + - 'Nonoperational statuses include: "Error", "Starting", "Stopping", and "Service".' + - '"Service", could apply during mirror-resilvering of a disk, reload of a user permissions list, or other administrative work.' + - Not all such work is online, yet the managed element is neither "OK" nor in one of the other states. + returned: always + type: str + sample: OK + status_info: + description: + - State of the logical device. If this property does not apply to the logical device, the value 5 (Not Applicable) should be used. + returned: always + type: int + system_creation_class_name: + description: Value of the scoping computer's CreationClassName property. + returned: always + type: str + sample: Win32_ComputerSystem + system_name: + description: Name of the scoping system. + returned: always + type: str + sample: WILMAR-TEST-123 + total_cylinders: + description: + - Total number of cylinders on the physical disk drive. + - 'Note: the value for this property is obtained through extended functions of BIOS interrupt 13h.' + - The value may be inaccurate if the drive uses a translation scheme to support high-capacity disk sizes. + - Consult the manufacturer for accurate drive specifications. + returned: always + type: int + sample: 6527 + total_heads: + description: + - Total number of heads on the disk drive. + - 'Note: the value for this property is obtained through extended functions of BIOS interrupt 13h.' + - The value may be inaccurate if the drive uses a translation scheme to support high-capacity disk sizes. + - Consult the manufacturer for accurate drive specifications. + returned: always + type: int + sample: 255 + total_sectors: + description: + - Total number of sectors on the physical disk drive. + - 'Note: the value for this property is obtained through extended functions of BIOS interrupt 13h.' + - The value may be inaccurate if the drive uses a translation scheme to support high-capacity disk sizes. + - Consult the manufacturer for accurate drive specifications. + returned: always + type: int + sample: 104856255 + total_tracks: + description: + - Total number of tracks on the physical disk drive. + - 'Note: the value for this property is obtained through extended functions of BIOS interrupt 13h.' + - The value may be inaccurate if the drive uses a translation scheme to support high-capacity disk sizes. + - Consult the manufacturer for accurate drive specifications. + returned: always + type: int + sample: 1664385 + tracks_per_cylinder: + description: + - Number of tracks in each cylinder on the physical disk drive. + - 'Note: the value for this property is obtained through extended functions of BIOS interrupt 13h.' + - The value may be inaccurate if the drive uses a translation scheme to support high-capacity disk sizes. + - Consult the manufacturer for accurate drive specifications. + returned: always + type: int + sample: 255 +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_disk_image.ps1 b/ansible_collections/community/windows/plugins/modules/win_disk_image.ps1 new file mode 100644 index 000000000..99ef3b5f6 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_disk_image.ps1 @@ -0,0 +1,79 @@ +#!powershell + +# Copyright: (c) 2017, Red Hat, Inc. +# 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" +Set-StrictMode -Version 2 + +If (-not (Get-Command Get-DiskImage -ErrorAction SilentlyContinue)) { + Fail-Json -message "win_disk_image requires Windows 8+ or Windows Server 2012+" +} + +$parsed_args = Parse-Args $args -supports_check_mode $true + +$result = @{ changed = $false } + +$image_path = Get-AnsibleParam $parsed_args "image_path" -failifempty $result +$state = Get-AnsibleParam $parsed_args "state" -default "present" -validateset "present", "absent" +$check_mode = Get-AnsibleParam $parsed_args "_ansible_check_mode" -default $false + +$di = Get-DiskImage $image_path + +If ($state -eq "present") { + If (-not $di.Attached) { + $result.changed = $true + + If (-not $check_mode) { + $di = Mount-DiskImage $image_path -PassThru + + # the actual mount is async, so the CIMInstance result may not immediately contain the data we need + $retry_count = 0 + While (-not $di.Attached -and $retry_count -lt 5) { + Start-Sleep -Seconds 1 > $null + $di = $di | Get-DiskImage + $retry_count++ + } + + If (-not $di.Attached) { + Fail-Json $result -message "Timed out waiting for disk to attach" + } + } + } + + # FUTURE: detect/handle "ejected" ISOs + # FUTURE: support explicit drive letter and NTFS in-volume mountpoints. + # VHDs don't always auto-assign, and other system settings can prevent automatic assignment + + If ($di.Attached) { + # only try to get the mount_path if the disk is attached ( + If ($di.StorageType -eq 1) { + # ISO, we can get the mountpoint directly from Get-Volume + $drive_letters = ($di | Get-Volume).DriveLetter + } + ElseIf ($di.StorageType -in @(2, 3)) { + # VHD/VHDX, need Get-Disk + Get-Partition to discover mountpoint + $drive_letters = ($di | Get-Disk | Get-Partition).DriveLetter + } + # remove any null entries (no drive letter) + $drive_letters = $drive_letters | Where-Object { $_ } + + If (-not $drive_letters) { + Fail-Json -message "Unable to retrieve drive letter from mounted image" + } + + $result.mount_paths = @($drive_letters | ForEach-Object { "$($_):\" }) + } +} +ElseIf ($state -eq "absent") { + If ($di.Attached) { + $result.changed = $true + If (-not $check_mode) { + Dismount-DiskImage $image_path > $null + } + } +} + +Exit-Json $result diff --git a/ansible_collections/community/windows/plugins/modules/win_disk_image.py b/ansible_collections/community/windows/plugins/modules/win_disk_image.py new file mode 100644 index 000000000..e2037da36 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_disk_image.py @@ -0,0 +1,57 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2017, Red Hat, Inc. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +module: win_disk_image +short_description: Manage ISO/VHD/VHDX mounts on Windows hosts +description: + - Manages mount behavior for a specified ISO, VHD, or VHDX image on a Windows host. When C(state) is C(present), + the image will be mounted under a system-assigned drive letter, which will be returned in the C(mount_path) value + of the module result. + - Requires Windows 8+ or Windows Server 2012+. +options: + image_path: + description: + - Path to an ISO, VHD, or VHDX image on the target Windows host (the file cannot reside on a network share) + type: str + required: yes + state: + description: + - Whether the image should be present as a drive-letter mount or not. + type: str + choices: [ absent, present ] + default: present +author: + - Matt Davis (@nitzmahone) +''' + +EXAMPLES = r''' +# Run installer from mounted ISO, then unmount +- name: Ensure an ISO is mounted + community.windows.win_disk_image: + image_path: C:\install.iso + state: present + register: disk_image_out + +- name: Run installer from mounted ISO + ansible.windows.win_package: + path: '{{ disk_image_out.mount_paths[0] }}setup\setup.exe' + product_id: 35a4e767-0161-46b0-979f-e61f282fee21 + state: present + +- name: Unmount ISO + community.windows.win_disk_image: + image_path: C:\install.iso + state: absent +''' + +RETURN = r''' +mount_paths: + description: A list of filesystem paths mounted from the target image. + returned: when C(state) is C(present) + type: list + sample: [ 'E:\', 'F:\' ] +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_dns_record.ps1 b/ansible_collections/community/windows/plugins/modules/win_dns_record.ps1 new file mode 100644 index 000000000..f19d9d3c4 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_dns_record.ps1 @@ -0,0 +1,212 @@ +#!powershell +# Copyright: (c) 2021 Sebastian Gruber ,dacoso GmbH All Rights Reserved. +# Copyright: (c) 2019, Hitachi ID Systems, Inc. +# SPDX-License-Identifier: GPL-3.0-only +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +#AnsibleRequires -CSharpUtil Ansible.Basic + +$spec = @{ + options = @{ + name = @{ type = "str"; required = $true } + port = @{ type = "int" } + priority = @{ type = "int" } + state = @{ type = "str"; choices = "absent", "present"; default = "present" } + ttl = @{ type = "int"; default = "3600" } + aging = @{ type = "bool"; default = $false } + type = @{ type = "str"; choices = "A", "AAAA", "CNAME", "DHCID", "NS", "PTR", "SRV", "TXT"; required = $true } + value = @{ type = "list"; elements = "str"; default = @() ; aliases = @( 'values' ) } + weight = @{ type = "int" } + zone = @{ type = "str"; required = $true } + computer_name = @{ type = "str" } + } + required_if = @(, @("type", "SRV", @("port", "priority", "weight"))) + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) +$name = $module.Params.name +$port = $module.Params.port +$priority = $module.Params.priority +$state = $module.Params.state +$ttl = $module.Params.ttl +$aging = $module.Params.aging +$type = $module.Params.type +$values = $module.Params.value +$weight = $module.Params.weight +$zone = $module.Params.zone +$dns_computer_name = $module.Params.computer_name + +$extra_args = @{} +$extra_args_new_records = @{} + +if ($null -ne $dns_computer_name) { + $extra_args.ComputerName = $dns_computer_name +} +if ($aging -eq $true) { + $extra_args_new_records.AgeRecord = $true +} + +if ($state -eq 'present') { + if ($values.Count -eq 0) { + $module.FailJson("Parameter 'values' must be non-empty when state='present'") + } +} +else { + if ($values.Count -ne 0) { + $module.FailJson("Parameter 'values' must be undefined or empty when state='absent'") + } +} + +# TODO: add warning for forest minTTL override -- see https://docs.microsoft.com/en-us/windows/desktop/ad/configuration-of-ttl-limits +if ($ttl -lt 1 -or $ttl -gt 31557600) { + $module.FailJson("Parameter 'ttl' must be between 1 and 31557600") +} + +$ttl = New-TimeSpan -Seconds $ttl + +if (($type -eq 'CNAME' -or $type -eq 'NS' -or $type -eq 'PTR' -or $type -eq 'SRV') -and $null -ne $values -and $values.Count -gt 0 -and $zone[-1] -ne '.') { + # CNAMEs and PTRs should be '.'-terminated, or record matching will fail + $values = $values | ForEach-Object { + if ($_ -Like "*.") { $_ } else { "$_." } + } +} + +$record_argument_name = @{ + A = "IPv4Address" + AAAA = "IPv6Address" + CNAME = "HostNameAlias" + DHCID = "DhcpIdentifier" + # MX = "MailExchange" + NS = "NameServer" + PTR = "PtrDomainName" + SRV = "DomainName" + TXT = "DescriptiveText" +}[$type] + +function Get-DnsServerResourceRecordDataPropertyName { + Switch -Exact ($type) { + 'DHCID' { + 'DhcId' + } + default { + $record_argument_name + } + } +} + +$changes = @{ + before = "" + after = "" +} + +$records = Get-DnsServerResourceRecord -ZoneName $zone -Name $name -RRType $type -Node -ErrorAction:Ignore @extra_args | Sort-Object + +if ($null -ne $records) { + # We use [Hashtable]$required_values below as a set rather than a map. + # It provides quick lookup to test existing DNS record against. By removing + # items as each is processed, whatever remains at the end is missing + # content (that needs to be added). + $required_values = @{} + foreach ($value in $values) { + $required_values[$value.ToString()] = $null + } + + foreach ($record in $records) { + # check, if record is aging + $record_aging_old = ($null -ne $record.Timestamp) + + $record_value = $record.RecordData.$(Get-DnsServerResourceRecordDataPropertyName).ToString() + if ((-Not $required_values.ContainsKey($record_value)) -Or (-Not $record_aging_old -eq $aging)) { + $record | Remove-DnsServerResourceRecord -ZoneName $zone -Force -WhatIf:$module.CheckMode @extra_args + $changes.before += "[$zone] $($record.HostName) $($record.TimeToLive.TotalSeconds) IN $type $record_value`n" + $module.Result.changed = $true + } + else { + if ($type -eq 'SRV') { + $record_port_old = $record.RecordData.Port.ToString() + $record_priority_old = $record.RecordData.Priority.ToString() + $record_weight_old = $record.RecordData.Weight.ToString() + + if ($record.TimeToLive -ne $ttl -or $port -ne $record_port_old -or $priority -ne $record_priority_old -or $weight -ne $record_weight_old) { + $new_record = $record.Clone() + $new_record.TimeToLive = $ttl + $new_record.RecordData.Port = $port + $new_record.RecordData.Priority = $priority + $new_record.RecordData.Weight = $weight + Set-DnsServerResourceRecord -ZoneName $zone -OldInputObject $record -NewInputObject $new_record -WhatIf:$module.CheckMode @extra_args + + $changes.before += -join @( + "[$zone] $($record.HostName) $($record.TimeToLive.TotalSeconds) IN " + "$type $record_value $record_port_old $record_weight_old $record_priority_old`n" + ) + $changes.after += "[$zone] $($record.HostName) $($ttl.TotalSeconds) IN $type $record_value $port $weight $priority`n" + $module.Result.changed = $true + } + } + else { + # This record matches one of the values; but does it match the TTL? + if ($record.TimeToLive -ne $ttl) { + $new_record = $record.Clone() + $new_record.TimeToLive = $ttl + Set-DnsServerResourceRecord -ZoneName $zone -OldInputObject $record -NewInputObject $new_record -WhatIf:$module.CheckMode @extra_args + $changes.before += "[$zone] $($record.HostName) $($record.TimeToLive.TotalSeconds) IN $type $record_value`n" + $changes.after += "[$zone] $($record.HostName) $($ttl.TotalSeconds) IN $type $record_value`n" + $module.Result.changed = $true + } + } + # Cross this one off the list, so we don't try adding it late + $required_values.Remove($record_value) + # Whatever is left in $required_values needs to be added + $values = $required_values.Keys + } + } +} + +if ($null -ne $values -and $values.Count -gt 0) { + foreach ($value in $values) { + $splat_args = @{ $type = $true; $record_argument_name = $value } + $module.Result.debug_splat_args = $splat_args + $srv_args = @{ + DomainName = $value + Weight = $weight + Priority = $priority + Port = $port + } + try { + if ($type -eq 'SRV') { + Add-DnsServerResourceRecord -SRV -Name $name -ZoneName $zone @srv_args @extra_args @extra_args_new_records -WhatIf:$module.CheckMode + } + else { + Add-DnsServerResourceRecord -Name $name -AllowUpdateAny -ZoneName $zone -TimeToLive $ttl @splat_args -WhatIf:$module.CheckMode ` + @extra_args @extra_args_new_records + } + } + catch { + $module.FailJson("Error adding DNS $type resource $name in zone $zone with value $value", $_) + } + $changes.after += "[$zone] $name $($ttl.TotalSeconds) IN $type $value`n" + } + $module.Result.changed = $true +} + +if ($module.CheckMode) { + # Simulated changes + $module.Diff.before = $changes.before + $module.Diff.after = $changes.after +} +else { + # Real changes + $records_end = Get-DnsServerResourceRecord -ZoneName $zone -Name $name -RRType $type -Node -ErrorAction:Ignore @extra_args | Sort-Object + $module.Diff.before = @( + $records | ForEach-Object { + "[$zone] $($_.HostName) $($_.TimeToLive.TotalSeconds) IN $type $($_.RecordData.$(Get-DnsServerResourceRecordDataPropertyName).ToString())`n" + } + ) -join '' + $module.Diff.after = @( + $records_end | ForEach-Object { + "[$zone] $($_.HostName) $($_.TimeToLive.TotalSeconds) IN $type $($_.RecordData.$(Get-DnsServerResourceRecordDataPropertyName).ToString())`n" + } + ) -join '' +} + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_dns_record.py b/ansible_collections/community/windows/plugins/modules/win_dns_record.py new file mode 100644 index 000000000..38692e311 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_dns_record.py @@ -0,0 +1,199 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021 Sebastian Gruber ,dacoso GmbH All Rights Reserved. +# Copyright: (c) 2019, Hitachi ID Systems, Inc. +# SPDX-License-Identifier: GPL-3.0-only +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_dns_record +short_description: Manage Windows Server DNS records +description: +- Manage DNS records within an existing Windows Server DNS zone. +author: + - Sebastian Gruber (@sgruber94) + - John Nelson (@johnboy2) +requirements: + - This module requires Windows 8, Server 2012, or newer. +options: + name: + description: + - The name of the record. + required: yes + type: str + port: + description: + - The port number of the record. + - Required when C(type=SRV). + - Supported only for C(type=SRV). + type: int + version_added: 1.0.0 + priority: + description: + - The priority number for each service in SRV record. + - Required when C(type=SRV). + - Supported only for C(type=SRV). + type: int + version_added: 1.0.0 + state: + description: + - Whether the record should exist or not. + choices: [ absent, present ] + default: present + type: str + ttl: + description: + - The "time to live" of the record, in seconds. + - Ignored when C(state=absent). + - Valid range is 1 - 31557600. + - Note that an Active Directory forest can specify a minimum TTL, and will + dynamically "round up" other values to that minimum. + default: 3600 + type: int + aging: + description: + - Should aging be activated for the record. + - If set to C(false), the record will be static. + default: false + type: bool + version_added: 1.13.0 + type: + description: + - The type of DNS record to manage. + - C(SRV) was added in the 1.0.0 release of this collection. + - C(NS) was added in the 1.1.0 release of this collection. + - C(TXT) was added in the 1.6.0 release of this collection. + - C(DHCID) was added in the 1.12.0 release of this collection. + choices: [ A, AAAA, CNAME, DHCID, NS, PTR, SRV, TXT ] + required: yes + type: str + value: + description: + - The value(s) to specify. Required when C(state=present). + - When C(type=PTR) only the partial part of the IP should be given. + - Multiple values can be passed when C(type=NS) + aliases: [ values ] + default: [] + type: list + elements: str + weight: + description: + - Weightage given to each service record in SRV record. + - Required when C(type=SRV). + - Supported only for C(type=SRV). + type: int + version_added: 1.0.0 + zone: + description: + - The name of the zone to manage (eg C(example.com)). + - The zone must already exist. + required: yes + type: str + computer_name: + description: + - Specifies a DNS server. + - You can specify an IP address or any value that resolves to an IP + address, such as a fully qualified domain name (FQDN), host name, or + NETBIOS name. + type: str +''' + +EXAMPLES = r''' +# Demonstrate creating a matching A and PTR record. + +- name: Create database server record + community.windows.win_dns_record: + name: "cgyl1404p.amer.example.com" + type: "A" + value: "10.1.1.1" + zone: "amer.example.com" + +- name: Create matching PTR record + community.windows.win_dns_record: + name: "1.1.1" + type: "PTR" + value: "db1" + zone: "10.in-addr.arpa" + +# Demonstrate replacing an A record with a CNAME + +- name: Remove static record + community.windows.win_dns_record: + name: "db1" + type: "A" + state: absent + zone: "amer.example.com" + +- name: Create database server alias + community.windows.win_dns_record: + name: "db1" + type: "CNAME" + value: "cgyl1404p.amer.example.com" + zone: "amer.example.com" + +# Demonstrate creating multiple A records for the same name + +- name: Create multiple A record values for www + community.windows.win_dns_record: + name: "www" + type: "A" + values: + - 10.0.42.5 + - 10.0.42.6 + - 10.0.42.7 + zone: "example.com" + +# Demonstrates a partial update (replace some existing values with new ones) +# for a pre-existing name + +- name: Update www host with new addresses + community.windows.win_dns_record: + name: "www" + type: "A" + values: + - 10.0.42.5 # this old value was kept (others removed) + - 10.0.42.12 # this new value was added + zone: "example.com" + +# Demonstrate creating a SRV record + +- name: Creating a SRV record with port number and priority + community.windows.win_dns_record: + name: "test" + priority: 5 + port: 995 + state: present + type: "SRV" + weight: 2 + value: "amer.example.com" + zone: "example.com" + +# Demonstrate creating a NS record with multiple values + +- name: Creating NS record + community.windows.win_dns_record: + name: "ansible.prog" + state: present + type: "NS" + values: + - 10.0.0.1 + - 10.0.0.2 + - 10.0.0.3 + - 10.0.0.4 + zone: "example.com" + +# Demonstrate creating a TXT record + +- name: Creating a TXT record with descriptive Text + community.windows.win_dns_record: + name: "test" + state: present + type: "TXT" + value: "justavalue" + zone: "example.com" +''' + +RETURN = r''' +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_dns_zone.ps1 b/ansible_collections/community/windows/plugins/modules/win_dns_zone.ps1 new file mode 100644 index 000000000..9b84f9d9f --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_dns_zone.ps1 @@ -0,0 +1,267 @@ +#!powershell + +# Copyright: (c) 2020 VMware, Inc. All Rights Reserved. +# SPDX-License-Identifier: GPL-3.0-only +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic + +$spec = @{ + options = @{ + name = @{ type = "str"; required = $true } + type = @{ type = "str"; choices = "primary", "secondary", "forwarder", "stub" } + replication = @{ type = "str"; choices = "forest", "domain", "legacy", "none" } + dynamic_update = @{ type = "str"; choices = "secure", "none", "nonsecureandsecure" } + state = @{ type = "str"; choices = "absent", "present"; default = "present" } + forwarder_timeout = @{ type = "int" } + dns_servers = @{ type = "list"; elements = "str" } + } + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) +$check_mode = $module.CheckMode + +$name = $module.Params.name +$type = $module.Params.type +$replication = $module.Params.replication +$dynamic_update = $module.Params.dynamic_update +$state = $module.Params.state +$dns_servers = $module.Params.dns_servers +$forwarder_timeout = $module.Params.forwarder_timeout + +$parms = @{ name = $name } + +Function Get-DnsZoneObject { + Param([PSObject]$Object) + $parms = @{ + name = $Object.ZoneName.toLower() + type = $Object.ZoneType.toLower() + paused = $Object.IsPaused + shutdown = $Object.IsShutdown + } + + if ($Object.DynamicUpdate) { $parms.dynamic_update = $Object.DynamicUpdate.toLower() } + if ($Object.IsReverseLookupZone) { $parms.reverse_lookup = $Object.IsReverseLookupZone } + if ($Object.ZoneType -like 'forwarder' ) { $parms.forwarder_timeout = $Object.ForwarderTimeout } + if ($Object.MasterServers) { $parms.dns_servers = $Object.MasterServers.IPAddressToString } + if (-not $Object.IsDsIntegrated) { + $parms.replication = "none" + $parms.zone_file = $Object.ZoneFile + } + else { + $parms.replication = $Object.ReplicationScope.toLower() + } + + return $parms | Sort-Object +} + +Function Compare-DnsZone { + Param( + [PSObject]$Original, + [PSObject]$Updated) + + if ($Original -eq $false) { return $false } + $props = @('ZoneType', 'DynamicUpdate', 'IsDsIntegrated', 'MasterServers', 'ForwarderTimeout', 'ReplicationScope') + $x = Compare-Object $Original $Updated -Property $props + $x.Count -eq 0 +} + +# attempt import of module +Try { Import-Module DnsServer } +Catch { $module.FailJson("The DnsServer module failed to load properly: $($_.Exception.Message)", $_) } + +Try { + # determine current zone state + $current_zone = Get-DnsServerZone -name $name + $module.Diff.before = Get-DnsZoneObject -Object $current_zone + if (-not $type) { $type = $current_zone.ZoneType.toLower() } + if ($current_zone.ZoneType -like $type) { $current_zone_type_match = $true } + # check for fast fails + if ($current_zone.ReplicationScope -like 'none' -and $replication -in @('legacy', 'forest', 'domain')) { + $module.FailJson("Converting a file backed DNS zone to Active Directory integrated zone is unsupported") + } + if ($current_zone.ReplicationScope -in @('legacy', 'forest', 'domain') -and $replication -like 'none') { + $module.FailJson("Converting Active Directory integrated zone to a file backed DNS zone is unsupported") + } + if ($current_zone.IsDsIntegrated -eq $false -and $parms.DynamicUpdate -eq 'secure') { + $module.FailJson("The secure dynamic update option is only available for Active Directory integrated zones") + } +} +Catch { + $module.Diff.before = "" + $current_zone = $false +} + +if ($state -eq "present") { + # parse replication/zonefile + if (-not $replication -and $current_zone) { + $parms.ReplicationScope = $current_zone.ReplicationScope + } + elseif ((($replication -eq 'none') -or (-not $replication)) -and (-not $current_zone)) { + $parms.ZoneFile = "$name.dns" + } + elseif (($replication -eq 'none') -and ($current_zone)) { + $parms.ZoneFile = "$name.dns" + } + else { + $parms.ReplicationScope = $replication + } + # parse param + if ($dynamic_update) { $parms.DynamicUpdate = $dynamic_update } + if ($dns_servers) { $parms.MasterServers = $dns_servers } + if ($type -in @('stub', 'forwarder', 'secondary') -and -not $current_zone -and -not $dns_servers) { + $module.FailJson("The dns_servers param is required when creating new stub, forwarder or secondary zones") + } + switch ($type) { + "primary" { + # remove irrelevant params + $parms.Remove('MasterServers') + if ($parms.ZoneFile -and ($dynamic_update -in @('secure', 'nonsecureandsecure'))) { + $parms.Remove('DynamicUpdate') + $module.Warn("Secure DNS updates are available only for Active Directory-integrated zones") + } + if (-not $current_zone) { + # create zone + Try { Add-DnsServerPrimaryZone @parms -WhatIf:$check_mode } + Catch { $module.FailJson("Failed to add $type zone $($name): $($_.Exception.Message)", $_) } + } + else { + # update zone + if (-not $current_zone_type_match) { + Try { + if ($current_zone.ReplicationScope) { + $parms.ReplicationScope = $current_zone.ReplicationScope + } + else { + $parms.Remove('ReplicationScope') + } + if ($current_zone.ZoneFile) { $parms.ZoneFile = $current_zone.ZoneFile } else { $parms.Remove('ReplicationScope') } + if ($current_zone.IsShutdown) { $module.FailJson("Failed to convert DNS zone $($name): this zone is shutdown and cannot be modified") } + ConvertTo-DnsServerPrimaryZone @parms -Force -WhatIf:$check_mode + } + Catch { $module.FailJson("Failed to convert DNS zone $($name): $($_.Exception.Message)", $_) } + } + Try { + if (-not $parms.ZoneFile) { Set-DnsServerPrimaryZone -Name $name -ReplicationScope $parms.ReplicationScope -WhatIf:$check_mode } + if ($dynamic_update) { Set-DnsServerPrimaryZone -Name $name -DynamicUpdate $parms.DynamicUpdate -WhatIf:$check_mode } + } + Catch { $module.FailJson("Failed to set properties on the zone $($name): $($_.Exception.Message)", $_) } + } + } + "secondary" { + # remove irrelevant params + $parms.Remove('ReplicationScope') + $parms.Remove('DynamicUpdate') + if (-not $current_zone) { + # enforce param + $parms.ZoneFile = "$name.dns" + # create zone + Try { Add-DnsServerSecondaryZone @parms -WhatIf:$check_mode } + Catch { $module.FailJson("Failed to add $type zone $($name): $($_.Exception.Message)", $_) } + } + else { + # update zone + if (-not $current_zone_type_match) { + $parms.MasterServers = $current_zone.MasterServers + $parms.ZoneFile = $current_zone.ZoneFile + if ($current_zone.IsShutdown) { $module.FailJson("Failed to convert DNS zone $($name): this zone is shutdown and cannot be modified") } + Try { ConvertTo-DnsServerSecondaryZone @parms -Force -WhatIf:$check_mode } + Catch { $module.FailJson("Failed to convert DNS zone $($name): $($_.Exception.Message)", $_) } + } + Try { if ($dns_servers) { Set-DnsServerSecondaryZone -Name $name -MasterServers $dns_servers -WhatIf:$check_mode } } + Catch { $module.FailJson("Failed to set properties on the zone $($name): $($_.Exception.Message)", $_) } + } + } + "stub" { + $parms.Remove('DynamicUpdate') + if (-not $current_zone) { + # create zone + Try { Add-DnsServerStubZone @parms -WhatIf:$check_mode } + Catch { $module.FailJson("Failed to add $type zone $($name): $($_.Exception.Message)", $_) } + } + else { + # update zone + if (-not $current_zone_type_match) { $module.FailJson("Failed to convert DNS zone $($name) to $type, unsupported conversion") } + Try { + if ($parms.ReplicationScope) { Set-DnsServerStubZone -Name $name -ReplicationScope $parms.ReplicationScope -WhatIf:$check_mode } + if ($forwarder_timeout) { Set-DnsServerStubZone -Name $name -ForwarderTimeout $forwarder_timeout -WhatIf:$check_mode } + if ($dns_servers) { Set-DnsServerStubZone -Name $name -MasterServers $dns_servers -WhatIf:$check_mode } + } + Catch { $module.FailJson("Failed to set properties on the zone $($name): $($_.Exception.Message)", $_) } + } + } + "forwarder" { + # remove irrelevant params + $parms.Remove('DynamicUpdate') + $parms.Remove('ZoneFile') + if ($forwarder_timeout -and ($forwarder_timeout -in 0..15)) { + $parms.ForwarderTimeout = $forwarder_timeout + } + if ($forwarder_timeout -and -not ($forwarder_timeout -in 0..15)) { + $module.Warn("The forwarder_timeout param must be an integer value between 0 and 15") + } + if ($parms.ReplicationScope -eq 'none') { $parms.Remove('ReplicationScope') } + if (-not $current_zone) { + # create zone + Try { Add-DnsServerConditionalForwarderZone @parms -WhatIf:$check_mode } + Catch { $module.FailJson("Failed to add $type zone $($name): $($_.Exception.Message)", $_) } + } + else { + # update zone + if (-not $current_zone_type_match) { $module.FailJson("Failed to convert DNS zone $($name) to $type, unsupported conversion") } + Try { + if ($parms.ReplicationScope) { + Set-DnsServerConditionalForwarderZone -Name $name -ReplicationScope $parms.ReplicationScope -WhatIf:$check_mode + } + if ($forwarder_timeout) { Set-DnsServerConditionalForwarderZone -Name $name -ForwarderTimeout $forwarder_timeout -WhatIf:$check_mode } + if ($dns_servers) { Set-DnsServerConditionalForwarderZone -Name $name -MasterServers $dns_servers -WhatIf:$check_mode } + } + Catch { $module.FailJson("Failed to set properties on the zone $($name): $($_.Exception.Message)", $_) } + } + } + } +} + +if ($state -eq "absent") { + if ($current_zone -and -not $check_mode) { + Try { + Remove-DnsServerZone -Name $name -Force -WhatIf:$check_mode + $module.Result.changed = $true + $module.Diff.after = "" + } + Catch { + $module.FailJson("Failed to remove DNS zone: $($_.Exception.Message)", $_) + } + } + $module.ExitJson() +} + +# determine if a change was made +Try { + $new_zone = Get-DnsServerZone -Name $name + if (-not (Compare-DnsZone -Original $current_zone -Updated $new_zone)) { + $module.Result.changed = $true + $module.Result.zone = Get-DnsZoneObject -Object $new_zone + $module.Diff.after = Get-DnsZoneObject -Object $new_zone + } + + # simulate changes if check mode + if ($check_mode) { + $new_zone = @{} + $current_zone.PSObject.Properties | ForEach-Object { + if ($parms[$_.Name]) { + $new_zone[$_.Name] = $parms[$_.Name] + } + else { + $new_zone[$_.Name] = $_.Value + } + } + $module.Diff.after = Get-DnsZoneObject -Object $new_zone + } +} +Catch { + $module.FailJson("Failed to lookup new zone $($name): $($_.Exception.Message)", $_) +} + +$module.ExitJson()
\ No newline at end of file diff --git a/ansible_collections/community/windows/plugins/modules/win_dns_zone.py b/ansible_collections/community/windows/plugins/modules/win_dns_zone.py new file mode 100644 index 000000000..194a3fc27 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_dns_zone.py @@ -0,0 +1,182 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020 VMware, Inc. All Rights Reserved. +# SPDX-License-Identifier: GPL-3.0-only +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_dns_zone +short_description: Manage Windows Server DNS Zones +author: Joe Zollo (@joezollo) +requirements: + - This module requires Windows Server 2012R2 or Newer +description: + - Manage Windows Server DNS Zones + - Adds, Removes and Modifies DNS Zones - Primary, Secondary, Forwarder & Stub + - Task should be delegated to a Windows DNS Server +options: + name: + description: + - Fully qualified name of the DNS zone. + type: str + required: true + type: + description: + - Specifies the type of DNS zone. + - When l(type=secondary), the DNS server will immediately attempt to + perform a zone transfer from the servers in this list. If this initial + transfer fails, then the zone will be left in an unworkable state. + This module does not verify the initial transfer. + type: str + choices: [ primary, secondary, stub, forwarder ] + dynamic_update: + description: + - Specifies how a zone handles dynamic updates. + - Secure DNS updates are available only for Active Directory-integrated + zones. + - When not specified during new zone creation, Windows will default this + to l(none). + type: str + choices: [ secure, none, nonsecureandsecure ] + state: + description: + - Specifies the desired state of the DNS zone. + - When l(state=present) the module will attempt to create the specified + DNS zone if it does not already exist. + - When l(state=absent), the module will remove the specified DNS + zone and all subsequent DNS records. + type: str + default: present + choices: [ present, absent ] + forwarder_timeout: + description: + - Specifies a length of time, in seconds, that a DNS server waits for a + remote DNS server to resolve a query. + - Accepts integer values between 0 and 15. + - If the provided value is not valid, it will be omitted and a warning + will be issued. + type: int + replication: + description: + - Specifies the replication scope for the DNS zone. + - l(replication=forest) will replicate the DNS zone to all domain + controllers in the Active Directory forest. + - l(replication=domain) will replicate the DNS zone to all domain + controllers in the Active Directory domain. + - l(replication=none) disables Active Directory integration and + creates a local file with the name of the zone. + - This is the equivalent of selecting l(store the zone in Active + Directory) in the GUI. + type: str + choices: [ forest, domain, legacy, none ] + dns_servers: + description: + - Specifies an list of IP addresses of the primary servers of the zone. + - DNS queries for a forwarded zone are sent to primary servers. + - Required if l(type=secondary), l(type=forwarder) or l(type=stub), + otherwise ignored. + - At least one server is required. + elements: str + type: list +''' + +EXAMPLES = r''' +- name: Ensure primary zone is present + community.windows.win_dns_zone: + name: wpinner.euc.vmware.com + replication: domain + type: primary + state: present + +- name: Ensure DNS zone is absent + community.windows.win_dns_zone: + name: jamals.euc.vmware.com + state: absent + +- name: Ensure forwarder has specific DNS servers + community.windows.win_dns_zone: + name: jamals.euc.vmware.com + type: forwarder + dns_servers: + - 10.245.51.100 + - 10.245.51.101 + - 10.245.51.102 + +- name: Ensure stub zone has specific DNS servers + community.windows.win_dns_zone: + name: virajp.euc.vmware.com + type: stub + dns_servers: + - 10.58.2.100 + - 10.58.2.101 + +- name: Ensure stub zone is converted to a secondary zone + community.windows.win_dns_zone: + name: virajp.euc.vmware.com + type: secondary + +- name: Ensure secondary zone is present with no replication + community.windows.win_dns_zone: + name: dgemzer.euc.vmware.com + type: secondary + replication: none + dns_servers: + - 10.19.20.1 + +- name: Ensure secondary zone is converted to a primary zone + community.windows.win_dns_zone: + name: dgemzer.euc.vmware.com + type: primary + replication: none + dns_servers: + - 10.19.20.1 + +- name: Ensure primary DNS zone is present without replication + community.windows.win_dns_zone: + name: basavaraju.euc.vmware.com + replication: none + type: primary + +- name: Ensure primary DNS zone has nonsecureandsecure dynamic updates enabled + community.windows.win_dns_zone: + name: basavaraju.euc.vmware.com + replication: none + dynamic_update: nonsecureandsecure + type: primary + +- name: Ensure DNS zone is absent + community.windows.win_dns_zone: + name: marshallb.euc.vmware.com + state: absent + +- name: Ensure DNS zones are absent + community.windows.win_dns_zone: + name: "{{ item }}" + state: absent + loop: + - jamals.euc.vmware.com + - dgemzer.euc.vmware.com + - wpinner.euc.vmware.com + - marshallb.euc.vmware.com + - basavaraju.euc.vmware.com +''' + +RETURN = r''' +zone: + description: New/Updated DNS zone parameters + returned: When l(state=present) + type: dict + sample: + name: + type: + dynamic_update: + reverse_lookup: + forwarder_timeout: + paused: + shutdown: + zone_file: + replication: + dns_servers: +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_domain_computer.ps1 b/ansible_collections/community/windows/plugins/modules/win_domain_computer.ps1 new file mode 100644 index 000000000..5e57c693b --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_domain_computer.ps1 @@ -0,0 +1,324 @@ +#!powershell + +# Copyright: (c) 2020, Brian Scholer (@briantist) +# Copyright: (c) 2017, AMTEGA - Xunta de Galicia +# 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.ArgvParser +#Requires -Module Ansible.ModuleUtils.CommandUtil +#Requires -Module ActiveDirectory + +# ------------------------------------------------------------------------------ +$ErrorActionPreference = "Stop" + +# Preparing result +$result = @{} +$result.changed = $false + +# Parameter ingestion +$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 +$temp = Get-AnsibleParam -obj $params -name '_ansible_remote_tmp' -type 'path' -default $env:TEMP + +$name = Get-AnsibleParam -obj $params -name "name" -failifempty $true -resultobj $result +$sam_account_name = Get-AnsibleParam -obj $params -name "sam_account_name" -default "${name}$" +If (-not $sam_account_name.EndsWith("$")) { + $sam_account_name = "${sam_account_name}$" +} +$enabled = Get-AnsibleParam -obj $params -name "enabled" -type "bool" -default $true +$description = Get-AnsibleParam -obj $params -name "description" -default $null +$domain_username = Get-AnsibleParam -obj $params -name "domain_username" -type "str" +$domain_password = Get-AnsibleParam -obj $params -name "domain_password" -type "str" -failifempty ($null -ne $domain_username) +$domain_server = Get-AnsibleParam -obj $params -name "domain_server" -type "str" +$state = Get-AnsibleParam -obj $params -name "state" -ValidateSet "present", "absent" -default "present" +$managed_by = Get-AnsibleParam -obj $params -name "managed_by" -type "str" + +$odj_action = Get-AnsibleParam -obj $params -name "offline_domain_join" -type "str" -ValidateSet "none", "output", "path" -default "none" +$_default_blob_path = Join-Path -Path $temp -ChildPath ([System.IO.Path]::GetRandomFileName()) +$odj_blob_path = Get-AnsibleParam -obj $params -name "odj_blob_path" -type "str" -default $_default_blob_path + +$extra_args = @{} +if ($null -ne $domain_username) { + $domain_password = ConvertTo-SecureString $domain_password -AsPlainText -Force + $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $domain_username, $domain_password + $extra_args.Credential = $credential +} +if ($null -ne $domain_server) { + $extra_args.Server = $domain_server +} + +# attempt import of module +try { + Import-Module ActiveDirectory +} +catch { + Fail-Json -obj $result -message "The ActiveDirectory module failed to load properly: $($_.Exception.Message)" +} + +If ($state -eq "present") { + $dns_hostname = Get-AnsibleParam -obj $params -name "dns_hostname" -failifempty $true -resultobj $result + $ou = Get-AnsibleParam -obj $params -name "ou" -failifempty $true -resultobj $result + $distinguished_name = "CN=$name,$ou" + + $desired_state = [ordered]@{ + name = $name + sam_account_name = $sam_account_name + dns_hostname = $dns_hostname + ou = $ou + distinguished_name = $distinguished_name + description = $description + enabled = $enabled + state = $state + managed_by = $managed_by + } +} +Else { + $desired_state = [ordered]@{ + name = $name + sam_account_name = $sam_account_name + state = $state + } +} + +# ------------------------------------------------------------------------------ +Function Get-InitialState($desired_state) { + # Test computer exists + $computer = Try { + Get-ADComputer ` + -Identity $desired_state.sam_account_name ` + -Properties DistinguishedName, DNSHostName, Enabled, Name, SamAccountName, Description, ObjectClass, ManagedBy ` + @extra_args + } + Catch { $null } + If ($computer) { + $null, $current_ou = $computer.DistinguishedName -split '(?<=[^\\](?:\\\\)*),' + $current_ou = $current_ou -join ',' + + $initial_state = [ordered]@{ + name = $computer.Name + sam_account_name = $computer.SamAccountName + dns_hostname = $computer.DNSHostName + ou = $current_ou + distinguished_name = $computer.DistinguishedName + description = $computer.Description + enabled = $computer.Enabled + state = "present" + managed_by = $computer.ManagedBy + } + } + Else { + $initial_state = [ordered]@{ + name = $desired_state.name + sam_account_name = $desired_state.sam_account_name + state = "absent" + } + } + + return $initial_state +} + +# ------------------------------------------------------------------------------ +Function Set-ConstructedState($initial_state, $desired_state) { + Try { + Set-ADComputer ` + -Identity $desired_state.name ` + -SamAccountName $desired_state.name ` + -DNSHostName $desired_state.dns_hostname ` + -Enabled $desired_state.enabled ` + -Description $desired_state.description ` + -ManagedBy $desired_state.managed_by ` + -WhatIf:$check_mode ` + @extra_args + } + Catch { + Fail-Json -obj $result -message "Failed to set the AD object $($desired_state.name): $($_.Exception.Message)" + } + + If ($initial_state.distinguished_name -cne $desired_state.distinguished_name) { + # Move computer to OU + Try { + Get-ADComputer -Identity $desired_state.sam_account_name @extra_args | + Move-ADObject ` + -TargetPath $desired_state.ou ` + -Confirm:$False ` + -WhatIf:$check_mode ` + @extra_args + } + Catch { + $msg = "Failed to move the AD object $($initial_state.distinguished_name) to $($desired_state.distinguished_name): $($_.Exception.Message)" + Fail-Json -obj $result -message $msg + } + } + $result.changed = $true +} + +# ------------------------------------------------------------------------------ +Function Add-ConstructedState($desired_state) { + Try { + New-ADComputer ` + -Name $desired_state.name ` + -SamAccountName $desired_state.sam_account_name ` + -DNSHostName $desired_state.dns_hostname ` + -Path $desired_state.ou ` + -Enabled $desired_state.enabled ` + -Description $desired_state.description ` + -ManagedBy $desired_state.managed_by ` + -WhatIf:$check_mode ` + @extra_args + } + Catch { + Fail-Json -obj $result -message "Failed to create the AD object $($desired_state.name): $($_.Exception.Message)" + } + + $result.changed = $true +} + +Function Invoke-OfflineDomainJoin { + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [Parameter(Mandatory = $true)] + [System.Collections.IDictionary] + $desired_state , + + [Parameter(Mandatory = $true)] + [ValidateSet('none', 'output', 'path')] + [String] + $Action , + + [Parameter()] + [System.IO.FileInfo] + $BlobPath + ) + + End { + if ($Action -eq 'none') { + return + } + + $dns_domain = $desired_state.dns_hostname -replace '^[^.]+\.' + + $output = $Action -eq 'output' + + $arguments = @( + 'djoin.exe' + '/PROVISION' + '/REUSE' # we're pre-creating the machine normally to set other fields, then overwriting it with this + '/DOMAIN' + $dns_domain + '/MACHINE' + $desired_state.sam_account_name.TrimEnd('$') # this machine name is the short name + '/MACHINEOU' + $desired_state.ou + '/SAVEFILE' + $BlobPath.FullName + ) + + $invocation = Argv-ToString -arguments $arguments + $result.djoin = @{ + invocation = $invocation + } + $result.odj_blob = '' + + if ($Action -eq 'path') { + $result.odj_blob_path = $BlobPath.FullName + } + + if (-not $BlobPath.Directory.Exists) { + Fail-Json -obj $result -message "BLOB path directory '$($BlobPath.Directory.FullName)' doesn't exist." + } + + if ($PSCmdlet.ShouldProcess($argstring)) { + try { + $djoin_result = Run-Command -command $invocation + $result.djoin.rc = $djoin_result.rc + $result.djoin.stdout = $djoin_result.stdout + $result.djoin.stderr = $djoin_result.stderr + + if ($djoin_result.rc) { + Fail-Json -obj $result -message "Problem running djoin.exe. See returned values." + } + + if ($output) { + $bytes = [System.IO.File]::ReadAllBytes($BlobPath.FullName) + $data = [Convert]::ToBase64String($bytes) + $result.odj_blob = $data + } + } + finally { + if ($output -and $BlobPath.Exists) { + $BlobPath.Delete() + } + } + } + } +} + +# ------------------------------------------------------------------------------ +Function Remove-ConstructedState($initial_state) { + Try { + Get-ADComputer -Identity $initial_state.sam_account_name @extra_args | + Remove-ADObject ` + -Recursive ` + -Confirm:$False ` + -WhatIf:$check_mode ` + @extra_args + } + Catch { + Fail-Json -obj $result -message "Failed to remove the AD object $($desired_state.name): $($_.Exception.Message)" + } + + $result.changed = $true +} + +# ------------------------------------------------------------------------------ +Function Test-HashtableEquality($x, $y) { + # Compare not nested HashTables + Foreach ($key in $x.Keys) { + If (($y.Keys -notcontains $key) -or ($x[$key] -cne $y[$key])) { + Return $false + } + } + foreach ($key in $y.Keys) { + if (($x.Keys -notcontains $key) -or ($x[$key] -cne $y[$key])) { + Return $false + } + } + Return $true +} + +# ------------------------------------------------------------------------------ +$initial_state = Get-InitialState($desired_state) + +If ($desired_state.state -eq "present") { + If ($initial_state.state -eq "present") { + $in_desired_state = Test-HashtableEquality -X $initial_state -Y $desired_state + + If (-not $in_desired_state) { + Set-ConstructedState -initial_state $initial_state -desired_state $desired_state + } + } + Else { + # $desired_state.state = "Present" & $initial_state.state = "Absent" + Add-ConstructedState -desired_state $desired_state + Invoke-OfflineDomainJoin -desired_state $desired_state -Action $odj_action -BlobPath $odj_blob_path -WhatIf:$check_mode + } +} +Else { + # $desired_state.state = "Absent" + If ($initial_state.state -eq "present") { + Remove-ConstructedState -initial_state $initial_state + } +} + +If ($diff_support) { + $diff = @{ + before = $initial_state + after = $desired_state + } + $result.diff = $diff +} + +Exit-Json -obj $result diff --git a/ansible_collections/community/windows/plugins/modules/win_domain_computer.py b/ansible_collections/community/windows/plugins/modules/win_domain_computer.py new file mode 100644 index 000000000..aff0f4687 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_domain_computer.py @@ -0,0 +1,211 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Brian Scholer (@briantist) +# Copyright: (c) 2017, AMTEGA - Xunta de Galicia +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_domain_computer +short_description: Manage computers in Active Directory +description: + - Create, read, update and delete computers in Active Directory using a + windows bridge computer to launch New-ADComputer, Get-ADComputer, + Set-ADComputer, Remove-ADComputer and Move-ADObject powershell commands. +options: + name: + description: + - Specifies the name of the object. + - This parameter sets the Name property of the Active Directory object. + - The LDAP display name (ldapDisplayName) of this property is name. + type: str + required: true + sam_account_name: + description: + - Specifies the Security Account Manager (SAM) account name of the + computer. + - It maximum is 256 characters, 15 is advised for older + operating systems compatibility. + - The LDAP display name (ldapDisplayName) for this property is sAMAccountName. + - If ommitted the value is the same as C(name). + - Note that all computer SAMAccountNames need to end with a C($). + - If C($) is omitted, it will be added to the end. + type: str + enabled: + description: + - Specifies if an account is enabled. + - An enabled account requires a password. + - This parameter sets the Enabled property for an account object. + - This parameter also sets the ADS_UF_ACCOUNTDISABLE flag of the + Active Directory User Account Control (UAC) attribute. + type: bool + default: yes + ou: + description: + - Specifies the X.500 path of the Organizational Unit (OU) or container + where the new object is created. Required when I(state=present). + - "Special characters must be escaped, + see L(Distinguished Names,https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ldap/distinguished-names) for details." + type: str + description: + description: + - Specifies a description of the object. + - This parameter sets the value of the Description property for the object. + - The LDAP display name (ldapDisplayName) for this property is description. + type: str + default: '' + dns_hostname: + description: + - Specifies the fully qualified domain name (FQDN) of the computer. + - This parameter sets the DNSHostName property for a computer object. + - The LDAP display name for this property is dNSHostName. + - Required when I(state=present). + type: str + domain_username: + description: + - The username to use when interacting with AD. + - If this is not set then the user Ansible used to log in with will be + used instead when using CredSSP or Kerberos with credential delegation. + type: str + domain_password: + description: + - The password for I(username). + type: str + domain_server: + description: + - Specifies the Active Directory Domain Services instance to connect to. + - Can be in the form of an FQDN or NetBIOS name. + - If not specified then the value is based on the domain of the computer + running PowerShell. + type: str + state: + description: + - Specified whether the computer should be C(present) or C(absent) in + Active Directory. + type: str + choices: [ absent, present ] + default: present + managed_by: + description: + - The value to be assigned to the LDAP C(managedBy) attribute. + - This value can be in the forms C(Distinguished Name), C(objectGUID), + C(objectSid) or C(sAMAccountName), see examples for more details. + type: str + version_added: '1.3.0' + offline_domain_join: + description: + - Provisions a computer in the directory and provides a BLOB file that can be used on the target computer/image to join it to the domain while offline. + - The C(none) value doesn't do any offline join operations. + - C(output) returns the BLOB in output. The BLOB should be treated as secret (it contains the machine password) so use C(no_log) when using this option. + - C(path) preserves the offline domain join BLOB file on the target machine for later use. The path will be returned. + - If the computer already exists, no BLOB will be created/returned, and the module will operate as it would have without offline domain join. + type: str + choices: + - none + - output + - path + default: none + odj_blob_path: + description: + - The path to the file where the BLOB will be saved. If omitted, a temporary file will be used. + - If I(offline_domain_join=output) the file will be deleted after its contents are returned. + - The parent directory for the BLOB file must exist; intermediate directories will not be created. +notes: + - "For more information on Offline Domain Join + see L(the step-by-step guide,https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd392267%28v=ws.10%29)." + - When using the ODJ BLOB to join a computer to the domain, it must be written out to a file. + - The file must be UTF-16 encoded (in PowerShell this encoding is called C(Unicode)), and it must end in a null character. See examples. + - The C(djoin.exe) part of the offline domain join process will not use I(domain_server), I(domain_username), or I(domain_password). + - This must be run on a host that has the ActiveDirectory powershell module installed. +seealso: +- module: ansible.windows.win_domain +- module: ansible.windows.win_domain_controller +- module: community.windows.win_domain_group +- module: ansible.windows.win_domain_membership +- module: community.windows.win_domain_user +author: +- Daniel Sánchez Fábregas (@Daniel-Sanchez-Fabregas) +- Brian Scholer (@briantist) +''' + +EXAMPLES = r''' + - name: Add linux computer to Active Directory OU using a windows machine + community.windows.win_domain_computer: + name: one_linux_server + sam_account_name: linux_server$ + dns_hostname: one_linux_server.my_org.local + ou: "OU=servers,DC=my_org,DC=local" + description: Example of linux server + enabled: yes + state: present + delegate_to: my_windows_bridge.my_org.local + + - name: Remove linux computer from Active Directory using a windows machine + community.windows.win_domain_computer: + name: one_linux_server + state: absent + delegate_to: my_windows_bridge.my_org.local + + - name: Provision a computer for offline domain join + community.windows.win_domain_computer: + name: newhost + dns_hostname: newhost.ansible.local + ou: 'OU=A great\, big organizational unit name,DC=ansible,DC=local' + state: present + offline_domain_join: yes + odj_return_blob: yes + register: computer_status + delegate_to: windc.ansible.local + + - name: Join a workgroup computer to the domain + vars: + target_blob_file: 'C:\ODJ\blob.txt' + ansible.windows.win_shell: | + $blob = [Convert]::FromBase64String('{{ computer_status.odj_blob }}') + [IO.File]::WriteAllBytes('{{ target_blob_file }}', $blob) + & djoin.exe --% /RequestODJ /LoadFile '{{ target_blob_file }}' /LocalOS /WindowsPath "%SystemRoot%" + + - name: Restart to complete domain join + ansible.windows.win_restart: +''' + +RETURN = r''' +odj_blob: + description: + - The offline domain join BLOB. This is an empty string when in check mode or when offline_domain_join is 'path'. + - This field contains the base64 encoded raw bytes of the offline domain join BLOB file. + returned: when offline_domain_join is not 'none' and the computer didn't exist + type: str + sample: <a long base64 string> +odj_blob_file: + description: The path to the offline domain join BLOB file on the target host. If odj_blob_path was specified, this will match that path. + returned: when offline_domain_join is 'path' and the computer didn't exist + type: str + sample: 'C:\Users\admin\AppData\Local\Temp\e4vxonty.rkb' +djoin: + description: Information about the invocation of djoin.exe. + returned: when offline_domain_join is True and the computer didn't exist + type: dict + contains: + invocation: + description: The full command line used to call djoin.exe + type: str + returned: always + sample: djoin.exe /PROVISION /MACHINE compname /MACHINEOU OU=Hosts,DC=ansible,DC=local /DOMAIN ansible.local /SAVEFILE blobfile.txt + rc: + description: The return code from djoin.exe + type: int + returned: when not check mode + sample: 87 + stdout: + description: The stdout from djoin.exe + type: str + returned: when not check mode + sample: Computer provisioning completed successfully. + stderr: + description: The stderr from djoin.exe + type: str + returned: when not check mode + sample: Invalid input parameter combination. +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_domain_group.ps1 b/ansible_collections/community/windows/plugins/modules/win_domain_group.ps1 new file mode 100644 index 000000000..d9373766c --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_domain_group.ps1 @@ -0,0 +1,371 @@ +#!powershell + +# Copyright: (c) 2017, Jordan Borean <jborean93@gmail.com>, and others +# 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 -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 + +$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true +$display_name = Get-AnsibleParam -obj $params -name "display_name" -type "str" +$domain_username = Get-AnsibleParam -obj $params -name "domain_username" -type "str" +$domain_password = Get-AnsibleParam -obj $params -name "domain_password" -type "str" -failifempty ($null -ne $domain_username) +$description = Get-AnsibleParam -obj $params -name "description" -type "str" +$category = Get-AnsibleParam -obj $params -name "category" -type "str" -validateset "distribution", "security" +$scope = Get-AnsibleParam -obj $params -name "scope" -type "str" -validateset "domainlocal", "global", "universal" +$managed_by = Get-AnsibleParam -obj $params -name "managed_by" -type "str" +$attributes = Get-AnsibleParam -obj $params -name "attributes" +$organizational_unit = Get-AnsibleParam -obj $params -name "organizational_unit" -type "str" -aliases "ou", "path" +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present", "absent" +$protect = Get-AnsibleParam -obj $params -name "protect" -type "bool" +$ignore_protection = Get-AnsibleParam -obj $params -name "ignore_protection" -type "bool" -default $false +$domain_server = Get-AnsibleParam -obj $params -name "domain_server" -type "str" + +$result = @{ + changed = $false + created = $false +} + +if ($diff_mode) { + $result.diff = @{} +} + +if (-not (Get-Module -Name ActiveDirectory -ListAvailable)) { + Fail-Json $result "win_domain_group requires the ActiveDirectory PS module to be installed" +} +Import-Module ActiveDirectory + +$extra_args = @{} +if ($null -ne $domain_username) { + $domain_password = ConvertTo-SecureString $domain_password -AsPlainText -Force + $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $domain_username, $domain_password + $extra_args.Credential = $credential +} +if ($null -ne $domain_server) { + $extra_args.Server = $domain_server +} + +try { + $group = Get-ADGroup -Identity $name -Properties * @extra_args +} +catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] { + $group = $null +} +catch { + Fail-Json $result "failed to retrieve initial details for group $($name): $($_.Exception.Message)" +} +if ($state -eq "absent") { + if ($null -ne $group) { + if ($group.ProtectedFromAccidentalDeletion -eq $true -and $ignore_protection -eq $true) { + $group = $group | Set-ADObject -ProtectedFromAccidentalDeletion $false -WhatIf:$check_mode -PassThru @extra_args + } + elseif ($group.ProtectedFromAccidentalDeletion -eq $true -and $ignore_protection -eq $false) { + $msg = -join @( + "cannot delete group $name when ProtectedFromAccidentalDeletion is turned on, " + "run this module with ignore_protection=true to override this" + ) + Fail-Json $result $msg + } + + try { + $group | Remove-ADGroup -Confirm:$false -WhatIf:$check_mode @extra_args + } + catch { + Fail-Json $result "failed to remove group $($name): $($_.Exception.Message)" + } + + $result.changed = $true + if ($diff_mode) { + $result.diff.prepared = "-[$name]" + } + } +} +else { + # validate that path is an actual path + if ($null -ne $organizational_unit) { + try { + Get-ADObject -Identity $organizational_unit @extra_args | Out-Null + } + catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] { + Fail-Json $result "the group path $organizational_unit does not exist, please specify a valid LDAP path" + } + } + + $diff_text = $null + if ($null -ne $group) { + # will be overridden later if no change actually occurs + $diff_text += "[$name]`n" + + # change the path of the group + if ($null -ne $organizational_unit) { + $group_cn = $group.CN + $existing_path = $group.DistinguishedName -replace "^CN=$group_cn,", '' + if ($existing_path -ne $organizational_unit) { + $protection_disabled = $false + if ($group.ProtectedFromAccidentalDeletion -eq $true -and $ignore_protection -eq $true) { + $group | Set-ADObject -ProtectedFromAccidentalDeletion $false -WhatIf:$check_mode -PassThru @extra_args | Out-Null + $protection_disabled = $true + } + elseif ($group.ProtectedFromAccidentalDeletion -eq $true -and $ignore_protection -eq $false) { + $msg = -join @( + "cannot move group $name when ProtectedFromAccidentalDeletion is turned on, " + "run this module with ignore_protection=true to override this" + ) + Fail-Json $result $msg + } + + try { + $group = $group | Move-ADObject -Targetpath $organizational_unit -WhatIf:$check_mode -PassThru @extra_args + } + catch { + Fail-Json $result "failed to move group from $existing_path to $($organizational_unit): $($_.Exception.Message)" + } + finally { + if ($protection_disabled -eq $true) { + $group | Set-ADObject -ProtectedFromAccidentalDeletion $true -WhatIf:$check_mode -PassThru @extra_args | Out-Null + } + } + + $result.changed = $true + $diff_text += "-DistinguishedName = CN=$group_cn,$existing_path`n+DistinguishedName = CN=$group_cn,$organizational_unit`n" + + if ($protection_disabled -eq $true) { + $group | Set-ADObject -ProtectedFromAccidentalDeletion $true -WhatIf:$check_mode @extra_args | Out-Null + } + # get the group again once we have moved it + $group = Get-ADGroup -Identity $name -Properties * @extra_args + } + } + + # change attributes of group + $extra_scope_change = $null + $run_change = $false + $set_args = $extra_args.Clone() + + if ($null -ne $scope) { + if ($group.GroupScope -ne $scope) { + # you cannot from from Global to DomainLocal and vice-versa, we + # need to change it to Universal and then finally to the target + # scope + if ($group.GroupScope -eq "global" -and $scope -eq "domainlocal") { + $set_args.GroupScope = "Universal" + $extra_scope_change = $scope + } + elseif ($group.GroupScope -eq "domainlocal" -and $scope -eq "global") { + $set_args.GroupScope = "Universal" + $extra_scope_change = $scope + } + else { + $set_args.GroupScope = $scope + } + $run_change = $true + $diff_text += "-GroupScope = $($group.GroupScope)`n+GroupScope = $scope`n" + } + } + + if ($null -ne $description -and $group.Description -cne $description) { + $set_args.Description = $description + $run_change = $true + $diff_text += "-Description = $($group.Description)`n+Description = $description`n" + } + + if ($null -ne $display_name -and $group.DisplayName -cne $display_name) { + $set_args.DisplayName = $display_name + $run_change = $true + $diff_text += "-DisplayName = $($group.DisplayName)`n+DisplayName = $display_name`n" + } + + if ($null -ne $category -and $group.GroupCategory -ne $category) { + $set_args.GroupCategory = $category + $run_change = $true + $diff_text += "-GroupCategory = $($group.GroupCategory)`n+GroupCategory = $category`n" + } + + if ($null -ne $managed_by) { + if ($null -eq $group.ManagedBy) { + $set_args.ManagedBy = $managed_by + $run_change = $true + $diff_text += "+ManagedBy = $managed_by`n" + } + else { + try { + $managed_by_object = Get-ADGroup -Identity $managed_by @extra_args + } + catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] { + try { + $managed_by_object = Get-ADUser -Identity $managed_by @extra_args + } + catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] { + Fail-Json $result "failed to find managed_by user or group $managed_by to be used for comparison" + } + } + + if ($group.ManagedBy -ne $managed_by_object.DistinguishedName) { + $set_args.ManagedBy = $managed_by + $run_change = $true + $diff_text += "-ManagedBy = $($group.ManagedBy)`n+ManagedBy = $($managed_by_object.DistinguishedName)`n" + } + } + } + + if ($null -ne $attributes) { + $add_attributes = @{} + $replace_attributes = @{} + foreach ($attribute in $attributes.GetEnumerator()) { + $attribute_name = $attribute.Name + $attribute_value = $attribute.Value + + $valid_property = [bool]($group.PSobject.Properties.name -eq $attribute_name) + if ($valid_property) { + $existing_value = $group.$attribute_name + if ($existing_value -cne $attribute_value) { + $replace_attributes.$attribute_name = $attribute_value + $diff_text += "-$attribute_name = $existing_value`n+$attribute_name = $attribute_value`n" + } + } + else { + $add_attributes.$attribute_name = $attribute_value + $diff_text += "+$attribute_name = $attribute_value`n" + } + } + if ($add_attributes.Count -gt 0) { + $set_args.Add = $add_attributes + $run_change = $true + } + if ($replace_attributes.Count -gt 0) { + $set_args.Replace = $replace_attributes + $run_change = $true + } + } + + if ($run_change) { + try { + $group = $group | Set-ADGroup -WhatIf:$check_mode -PassThru @set_args + } + catch { + Fail-Json $result "failed to change group $($name): $($_.Exception.Message)" + } + $result.changed = $true + + if ($null -ne $extra_scope_change) { + try { + $group = $group | Set-ADGroup -GroupScope $extra_scope_change -WhatIf:$check_mode -PassThru @extra_args + } + catch { + Fail-Json $result "failed to change scope of group $name to $($scope): $($_.Exception.Message)" + } + } + } + + # make sure our diff text is null if no change occurred + if ($result.changed -eq $false) { + $diff_text = $null + } + } + else { + # validate if scope is set + if ($null -eq $scope) { + Fail-Json $result "scope must be set when state=present and the group doesn't exist" + } + + $diff_text += "+[$name]`n+Scope = $scope`n" + $add_args = $extra_args.Clone() + $add_args.Name = $name + $add_args.GroupScope = $scope + + if ($null -ne $description) { + $add_args.Description = $description + $diff_text += "+Description = $description`n" + } + + if ($null -ne $display_name) { + $add_args.DisplayName = $display_name + $diff_text += "+DisplayName = $display_name`n" + } + + if ($null -ne $category) { + $add_args.GroupCategory = $category + $diff_text += "+GroupCategory = $category`n" + } + + if ($null -ne $managed_by) { + $add_args.ManagedBy = $managed_by + $diff_text += "+ManagedBy = $managed_by`n" + } + + if ($null -ne $attributes) { + $add_args.OtherAttributes = $attributes + foreach ($attribute in $attributes.GetEnumerator()) { + $diff_text += "+$($attribute.Name) = $($attribute.Value)`n" + } + } + + if ($null -ne $organizational_unit) { + $add_args.Path = $organizational_unit + $diff_text += "+Path = $organizational_unit`n" + } + + try { + $group = New-AdGroup -WhatIf:$check_mode -PassThru @add_args + } + catch { + Fail-Json $result "failed to create group $($name): $($_.Exception.Message)" + } + $result.changed = $true + $result.created = $true + } + + # set the protection value + if ($null -ne $protect) { + if (-not $check_mode) { + $group = Get-ADGroup -Identity $name -Properties * @extra_args + } + $existing_protection_value = $group.ProtectedFromAccidentalDeletion + if ($null -eq $existing_protection_value) { + $existing_protection_value = $false + } + if ($existing_protection_value -ne $protect) { + $diff_text += @" +-ProtectedFromAccidentalDeletion = $existing_protection_value ++ProtectedFromAccidentalDeletion = $protect +"@ + + $group | Set-ADObject -ProtectedFromAccidentalDeletion $protect -WhatIf:$check_mode -PassThru @extra_args + $result.changed = $true + } + } + + if ($diff_mode -and $null -ne $diff_text) { + $result.diff.prepared = $diff_text + } + + if (-not $check_mode) { + $group = Get-ADGroup -Identity $name -Properties * @extra_args + $result.sid = $group.SID.Value + $result.description = $group.Description + $result.distinguished_name = $group.DistinguishedName + $result.display_name = $group.DisplayName + $result.name = $group.Name + $result.canonical_name = $group.CanonicalName + $result.guid = $group.ObjectGUID + $result.protected_from_accidental_deletion = $group.ProtectedFromAccidentalDeletion + $result.managed_by = $group.ManagedBy + $result.group_scope = ($group.GroupScope).ToString() + $result.category = ($group.GroupCategory).ToString() + + if ($null -ne $attributes) { + $result.attributes = @{} + foreach ($attribute in $attributes.GetEnumerator()) { + $attribute_name = $attribute.Name + $result.attributes.$attribute_name = $group.$attribute_name + } + } + } +} + +Exit-Json $result diff --git a/ansible_collections/community/windows/plugins/modules/win_domain_group.py b/ansible_collections/community/windows/plugins/modules/win_domain_group.py new file mode 100644 index 000000000..b761055e8 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_domain_group.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) + +DOCUMENTATION = r''' +--- +module: win_domain_group +short_description: Creates, modifies or removes domain groups +description: +- Creates, modifies or removes groups in Active Directory. +- For local groups, use the M(ansible.windows.win_group) module instead. +options: + attributes: + description: + - A dict of custom LDAP attributes to set on the group. + - This can be used to set custom attributes that are not exposed as module + parameters, e.g. C(mail). + - See the examples on how to format this parameter. + type: dict + category: + description: + - The category of the group, this is the value to assign to the LDAP + C(groupType) attribute. + - If a new group is created then C(security) will be used by default. + type: str + choices: [ distribution, security ] + description: + description: + - The value to be assigned to the LDAP C(description) attribute. + type: str + display_name: + description: + - The value to assign to the LDAP C(displayName) attribute. + type: str + domain_username: + description: + - The username to use when interacting with AD. + - If this is not set then the user Ansible used to log in with will be + used instead. + type: str + domain_password: + description: + - The password for C(username). + type: str + domain_server: + description: + - Specifies the Active Directory Domain Services instance to connect to. + - Can be in the form of an FQDN or NetBIOS name. + - If not specified then the value is based on the domain of the computer + running PowerShell. + type: str + ignore_protection: + description: + - Will ignore the C(ProtectedFromAccidentalDeletion) flag when deleting or + moving a group. + - The module will fail if one of these actions need to occur and this value + is set to C(no). + type: bool + default: no + managed_by: + description: + - The value to be assigned to the LDAP C(managedBy) attribute. + - This value can be in the forms C(Distinguished Name), C(objectGUID), + C(objectSid) or C(sAMAccountName), see examples for more details. + type: str + name: + description: + - The name of the group to create, modify or remove. + - This value can be in the forms C(Distinguished Name), C(objectGUID), + C(objectSid) or C(sAMAccountName), see examples for more details. + type: str + required: yes + organizational_unit: + description: + - The full LDAP path to create or move the group to. + - This should be the path to the parent object to create or move the group to. + - See examples for details of how this path is formed. + type: str + aliases: [ ou, path ] + protect: + description: + - Will set the C(ProtectedFromAccidentalDeletion) flag based on this value. + - This flag stops a user from deleting or moving a group to a different + path. + type: bool + scope: + description: + - The scope of the group. + - If C(state=present) and the group doesn't exist then this must be set. + type: str + choices: [domainlocal, global, universal] + state: + description: + - If C(state=present) this module will ensure the group is created and is + configured accordingly. + - If C(state=absent) this module will delete the group if it exists + type: str + choices: [ absent, present ] + default: present +notes: +- This must be run on a host that has the ActiveDirectory powershell module installed. +seealso: +- module: ansible.windows.win_domain +- module: ansible.windows.win_domain_controller +- module: community.windows.win_domain_computer +- module: ansible.windows.win_domain_membership +- module: community.windows.win_domain_user +- module: ansible.windows.win_group +- module: ansible.windows.win_group_membership +author: +- Jordan Borean (@jborean93) +''' + +EXAMPLES = r''' +- name: Ensure the group Cow exists using sAMAccountName + community.windows.win_domain_group: + name: Cow + scope: global + path: OU=groups,DC=ansible,DC=local + +- name: Ensure the group Cow doesn't exist using the Distinguished Name + community.windows.win_domain_group: + name: CN=Cow,OU=groups,DC=ansible,DC=local + state: absent + +- name: Delete group ignoring the protection flag + community.windows.win_domain_group: + name: Cow + state: absent + ignore_protection: yes + +- name: Create group with delete protection enabled and custom attributes + community.windows.win_domain_group: + name: Ansible Users + scope: domainlocal + category: security + attributes: + mail: helpdesk@ansible.com + wWWHomePage: www.ansible.com + ignore_protection: yes + +- name: Change the OU of a group using the SID and ignore the protection flag + community.windows.win_domain_group: + name: S-1-5-21-2171456218-3732823212-122182344-1189 + scope: global + organizational_unit: OU=groups,DC=ansible,DC=local + ignore_protection: yes + +- name: Add managed_by user + community.windows.win_domain_group: + name: Group Name Here + managed_by: Domain Admins + +- name: Add group and specify the AD domain services to use for the create + community.windows.win_domain_group: + name: Test Group + domain_username: user@CORP.ANSIBLE.COM + domain_password: Password01! + domain_server: corp-DC12.corp.ansible.com + scope: domainlocal +''' + +RETURN = r''' +attributes: + description: Custom attributes that were set by the module. This does not + show all the custom attributes rather just the ones that were set by the + module. + returned: group exists and attributes are set on the module invocation + type: dict + sample: + mail: 'helpdesk@ansible.com' + wWWHomePage: 'www.ansible.com' +canonical_name: + description: The canonical name of the group. + returned: group exists + type: str + sample: ansible.local/groups/Cow +category: + description: The Group type value of the group, i.e. Security or Distribution. + returned: group exists + type: str + sample: Security +description: + description: The Description of the group. + returned: group exists + type: str + sample: Group Description +display_name: + description: The Display name of the group. + returned: group exists + type: str + sample: Users who connect through RDP +distinguished_name: + description: The full Distinguished Name of the group. + returned: group exists + type: str + sample: CN=Cow,OU=groups,DC=ansible,DC=local +group_scope: + description: The Group scope value of the group. + returned: group exists + type: str + sample: Universal +guid: + description: The guid of the group. + returned: group exists + type: str + sample: 512a9adb-3fc0-4a26-9df0-e6ea1740cf45 +managed_by: + description: The full Distinguished Name of the AD object that is set on the + managedBy attribute. + returned: group exists + type: str + sample: CN=Domain Admins,CN=Users,DC=ansible,DC=local +name: + description: The name of the group. + returned: group exists + type: str + sample: Cow +protected_from_accidental_deletion: + description: Whether the group is protected from accidental deletion. + returned: group exists + type: bool + sample: true +sid: + description: The Security ID of the group. + returned: group exists + type: str + sample: S-1-5-21-2171456218-3732823212-122182344-1189 +created: + description: Whether a group was created + returned: always + type: bool + sample: true +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_domain_group_membership.ps1 b/ansible_collections/community/windows/plugins/modules/win_domain_group_membership.ps1 new file mode 100644 index 000000000..6393e3c54 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_domain_group_membership.ps1 @@ -0,0 +1,136 @@ +#!powershell + +# Copyright: (c) 2019, Marius Rieder <marius.rieder@scs.ch> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#Requires -Module Ansible.ModuleUtils.Legacy + +try { + Import-Module ActiveDirectory +} +catch { + Fail-Json -obj @{} -message "win_domain_group_membership requires the ActiveDirectory PS module to be installed" +} + +$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 + +# Module control parameters +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present", "absent", "pure" +$domain_username = Get-AnsibleParam -obj $params -name "domain_username" -type "str" +$domain_password = Get-AnsibleParam -obj $params -name "domain_password" -type "str" -failifempty ($null -ne $domain_username) +$domain_server = Get-AnsibleParam -obj $params -name "domain_server" -type "str" + +# Group Membership parameters +$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true +$members = Get-AnsibleParam -obj $params -name "members" -type "list" -failifempty $true + +# Filter ADObjects by ObjectClass +$ad_object_class_filter = "(ObjectClass -eq 'user' -or ObjectClass -eq 'group' -or ObjectClass -eq 'computer' -or ObjectClass -eq 'msDS-ManagedServiceAccount')" + +$extra_args = @{} +if ($null -ne $domain_username) { + $domain_password = ConvertTo-SecureString $domain_password -AsPlainText -Force + $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $domain_username, $domain_password + $extra_args.Credential = $credential +} +if ($null -ne $domain_server) { + $extra_args.Server = $domain_server +} + +$ADGroup = Get-ADGroup -Identity $name @extra_args + +$result = @{ + changed = $false + added = [System.Collections.Generic.List`1[String]]@() + removed = [System.Collections.Generic.List`1[String]]@() +} +if ($diff_mode) { + $result.diff = @{} +} + +$filter = "(memberOf=$($ADGroup.DistinguishedName))" + +$members_before = Get-ADObject -LDAPFilter $filter -Properties sAMAccountName, objectSID @extra_args +$pure_members = [System.Collections.Generic.List`1[String]]@() + +foreach ($member in $members) { + $extra_member_args = $extra_args.Clone() + if ($member -match "\\") { + $extra_member_args.Server = $member.Split("\")[0] + $member = $member.Split("\")[1] + } + $group_member = Get-ADObject -Filter "SamAccountName -eq '$member' -and $ad_object_class_filter" -Properties objectSid, sAMAccountName @extra_member_args + if (!$group_member) { + Fail-Json -obj $result "Could not find domain user, group, service account or computer named $member" + } + + if ($state -eq "pure") { + $pure_members.Add($group_member.objectSid) + } + + $user_in_group = $false + foreach ($current_member in $members_before) { + if ($current_member.objectSid -eq $group_member.objectSid) { + $user_in_group = $true + break + } + } + + if ($state -in @("present", "pure") -and !$user_in_group) { + Add-ADPrincipalGroupMembership -Identity $group_member -MemberOf $ADGroup -WhatIf:$check_mode @extra_member_args + $result.added.Add($group_member.SamAccountName) + $result.changed = $true + } + elseif ($state -eq "absent" -and $user_in_group) { + Remove-ADPrincipalGroupMembership -Identity $group_member -MemberOf $ADGroup -WhatIf:$check_mode -Confirm:$False @extra_member_args + $result.removed.Add($group_member.SamAccountName) + $result.changed = $true + } +} + +if ($state -eq "pure") { + # Perform removals for existing group members not defined in $members + $current_members = Get-ADObject -LDAPFilter $filter -Properties sAMAccountName, objectSID @extra_args + + foreach ($current_member in $current_members) { + $user_to_remove = $true + foreach ($pure_member in $pure_members) { + if ($pure_member -eq $current_member.objectSid) { + $user_to_remove = $false + break + } + } + + if ($user_to_remove) { + Remove-ADPrincipalGroupMembership -Identity $current_member -MemberOf $ADGroup -WhatIf:$check_mode -Confirm:$False @extra_member_args + $result.removed.Add($current_member.SamAccountName) + $result.changed = $true + } + } +} + +$final_members = Get-ADObject -LDAPFilter $filter -Properties sAMAccountName, objectSID @extra_args + +if ($final_members) { + $result.members = [Array]$final_members.SamAccountName +} +else { + $result.members = @() +} + +if ($diff_mode -and $result.changed) { + $result.diff.before = $members_before.SamAccountName | Out-String + if (!$check_mode) { + $result.diff.after = [Array]$final_members.SamAccountName | Out-String + } + else { + $after = [System.Collections.Generic.List`1[String]]$result.members + $result.removed | ForEach-Object { $after.Remove($_) > $null } + $after.AddRange($result.added) + $result.diff.after = $after | Out-String + } +} + +Exit-Json -obj $result diff --git a/ansible_collections/community/windows/plugins/modules/win_domain_group_membership.py b/ansible_collections/community/windows/plugins/modules/win_domain_group_membership.py new file mode 100644 index 000000000..5e10ac3b2 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_domain_group_membership.py @@ -0,0 +1,126 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2017, Andrew Saraceni <andrew.saraceni@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_domain_group_membership +short_description: Manage Windows domain group membership +description: + - Allows the addition and removal of domain users + and domain groups from/to a domain group. +options: + name: + description: + - Name of the domain group to manage membership on. + type: str + required: yes + members: + description: + - A list of members to ensure are present/absent from the group. + - The given names must be a SamAccountName of a user, group, service account, or computer. + - For computers, you must add "$" after the name; for example, to add "Mycomputer" to a group, use "Mycomputer$" as the member. + - If the member object is part of another domain in a multi-domain forest, you must add the domain and "\" in front of the name. + type: list + elements: str + required: yes + state: + description: + - Desired state of the members in the group. + - When C(state) is C(pure), only the members specified will exist, + and all other existing members not specified are removed. + type: str + choices: [ absent, present, pure ] + default: present + domain_username: + description: + - The username to use when interacting with AD. + - If this is not set then the user Ansible used to log in with will be + used instead when using CredSSP or Kerberos with credential delegation. + type: str + domain_password: + description: + - The password for I(username). + type: str + domain_server: + description: + - Specifies the Active Directory Domain Services instance to connect to. + - Can be in the form of an FQDN or NetBIOS name. + - If not specified then the value is based on the domain of the computer + running PowerShell. + type: str +notes: +- This must be run on a host that has the ActiveDirectory powershell module installed. +seealso: +- module: community.windows.win_domain_user +- module: community.windows.win_domain_group +author: + - Marius Rieder (@jiuka) +''' + +EXAMPLES = r''' +- name: Add a domain user/group to a domain group + community.windows.win_domain_group_membership: + name: Foo + members: + - Bar + state: present + +- name: Remove a domain user/group from a domain group + community.windows.win_domain_group_membership: + name: Foo + members: + - Bar + state: absent + +- name: Ensure only a domain user/group exists in a domain group + community.windows.win_domain_group_membership: + name: Foo + members: + - Bar + state: pure + +- name: Add a computer to a domain group + community.windows.win_domain_group_membership: + name: Foo + members: + - DESKTOP$ + state: present + +- name: Add a domain user/group from another Domain in the multi-domain forest to a domain group + community.windows.win_domain_group_membership: + domain_server: DomainAAA.cloud + name: GroupinDomainAAA + members: + - DomainBBB.cloud\UserInDomainBBB + state: Present + +''' + +RETURN = r''' +name: + description: The name of the target domain group. + returned: always + type: str + sample: Domain-Admins +added: + description: A list of members added when C(state) is C(present) or + C(pure); this is empty if no members are added. + returned: success and C(state) is C(present) or C(pure) + type: list + sample: ["UserName", "GroupName"] +removed: + description: A list of members removed when C(state) is C(absent) or + C(pure); this is empty if no members are removed. + returned: success and C(state) is C(absent) or C(pure) + type: list + sample: ["UserName", "GroupName"] +members: + description: A list of all domain group members at completion; this is empty + if the group contains no members. + returned: success + type: list + sample: ["UserName", "GroupName"] +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_domain_object_info.ps1 b/ansible_collections/community/windows/plugins/modules/win_domain_object_info.ps1 new file mode 100644 index 000000000..9ff7a5ed7 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_domain_object_info.ps1 @@ -0,0 +1,295 @@ +#!powershell + +# Copyright: (c) 2020, 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.AddType +#Requires -Module ActiveDirectory + +$spec = @{ + options = @{ + domain_password = @{ type = 'str'; no_log = $true } + domain_server = @{ type = 'str' } + domain_username = @{ type = 'str' } + filter = @{ type = 'str' } + identity = @{ type = 'str' } + include_deleted = @{ type = 'bool'; default = $false } + ldap_filter = @{ type = 'str' } + properties = @{ type = 'list'; elements = 'str' } + search_base = @{ type = 'str' } + search_scope = @{ type = 'str'; choices = @('base', 'one_level', 'subtree') } + } + supports_check_mode = $true + mutually_exclusive = @( + @('filter', 'identity', 'ldap_filter'), + @('identity', 'search_base'), + @('identity', 'search_scope') + ) + required_one_of = @( + , @('filter', 'identity', 'ldap_filter') + ) + required_together = @(, @('domain_username', 'domain_password')) +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$module.Result.objects = @() # Always ensure this is returned even in a failure. + +$domainServer = $module.Params.domain_server +$domainPassword = $module.Params.domain_password +$domainUsername = $module.Params.domain_username +$filter = $module.Params.filter +$identity = $module.Params.identity +$includeDeleted = $module.Params.include_deleted +$ldapFilter = $module.Params.ldap_filter +$properties = $module.Params.properties +$searchBase = $module.Params.search_base +$searchScope = $module.Params.search_scope + +$credential = $null +if ($domainUsername) { + $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @( + $domainUsername, + (ConvertTo-SecureString -AsPlainText -Force -String $domainPassword) + ) +} + +Add-CSharpType -References @' +using System; + +namespace Ansible.WinDomainObjectInfo +{ + [Flags] + public enum UserAccountControl : int + { + ADS_UF_SCRIPT = 0x00000001, + ADS_UF_ACCOUNTDISABLE = 0x00000002, + ADS_UF_HOMEDIR_REQUIRED = 0x00000008, + ADS_UF_LOCKOUT = 0x00000010, + ADS_UF_PASSWD_NOTREQD = 0x00000020, + ADS_UF_PASSWD_CANT_CHANGE = 0x00000040, + ADS_UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED = 0x00000080, + ADS_UF_TEMP_DUPLICATE_ACCOUNT = 0x00000100, + ADS_UF_NORMAL_ACCOUNT = 0x00000200, + ADS_UF_INTERDOMAIN_TRUST_ACCOUNT = 0x00000800, + ADS_UF_WORKSTATION_TRUST_ACCOUNT = 0x00001000, + ADS_UF_SERVER_TRUST_ACCOUNT = 0x00002000, + ADS_UF_DONT_EXPIRE_PASSWD = 0x00010000, + ADS_UF_MNS_LOGON_ACCOUNT = 0x00020000, + ADS_UF_SMARTCARD_REQUIRED = 0x00040000, + ADS_UF_TRUSTED_FOR_DELEGATION = 0x00080000, + ADS_UF_NOT_DELEGATED = 0x00100000, + ADS_UF_USE_DES_KEY_ONLY = 0x00200000, + ADS_UF_DONT_REQUIRE_PREAUTH = 0x00400000, + ADS_UF_PASSWORD_EXPIRED = 0x00800000, + ADS_UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION = 0x01000000, + } + + public enum sAMAccountType : int + { + SAM_DOMAIN_OBJECT = 0x00000000, + SAM_GROUP_OBJECT = 0x10000000, + SAM_NON_SECURITY_GROUP_OBJECT = 0x10000001, + SAM_ALIAS_OBJECT = 0x20000000, + SAM_NON_SECURITY_ALIAS_OBJECT = 0x20000001, + SAM_USER_OBJECT = 0x30000000, + SAM_NORMAL_USER_ACCOUNT = 0x30000000, + SAM_MACHINE_ACCOUNT = 0x30000001, + SAM_TRUST_ACCOUNT = 0x30000002, + SAM_APP_BASIC_GROUP = 0x40000000, + SAM_APP_QUERY_GROUP = 0x40000001, + SAM_ACCOUNT_TYPE_MAX = 0x7fffffff, + } +} +'@ + +Function ConvertTo-OutputValue { + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $true)] + [AllowNull()] + [Object] + $InputObject + ) + + if ($InputObject -is [System.Security.Principal.SecurityIdentifier]) { + # Syntax: SID - Only serialize the SID as a string and not the other metadata properties. + $sidInfo = @{ + Sid = $InputObject.Value + } + + # Try and map the SID to the account name, this may fail if the SID is invalid or not mappable. + try { + $sidInfo.Name = $InputObject.Translate([System.Security.Principal.NTAccount]).Value + } + catch [System.Security.Principal.IdentityNotMappedException] { + $sidInfo.Name = $null + } + + $sidInfo + } + elseif ($InputObject -is [Byte[]]) { + # Syntax: Octet String - By default will serialize as a list of decimal values per byte, instead return a + # Base64 string as Ansible can easily parse that. + [System.Convert]::ToBase64String($InputObject) + } + elseif ($InputObject -is [DateTime]) { + # Syntax: UTC Coded Time - .NET DateTimes serialized as in the form "Date(FILETIME)" which isn't easily + # parsable by Ansible, instead return as an ISO 8601 string in the UTC timezone. + [TimeZoneInfo]::ConvertTimeToUtc($InputObject).ToString("o") + } + elseif ($InputObject -is [System.Security.AccessControl.ObjectSecurity]) { + # Complex object which isn't easily serializable. Instead we should just return the SDDL string. If a user + # needs to parse this then they really need to reprocess the SDDL string and process their results on another + # win_shell task. + $InputObject.GetSecurityDescriptorSddlForm(([System.Security.AccessControl.AccessControlSections]::All)) + } + else { + # Syntax: (All Others) - The default serialization handling of other syntaxes are fine, don't do anything. + $InputObject + } +} + +# attempt import of module +try { + Import-Module ActiveDirectory +} +catch { + $module.FailJson("The ActiveDirectory module failed to load properly: $($_.Exception.Message)", $_) +} + +<# +Calling Get-ADObject that returns multiple objects with -Properties * will only return the properties that were set on +the first found object. To counter this problem we will first call Get-ADObject to list all the objects that match the +filter specified then get the properties on each object. +#> + +$commonParams = @{ + IncludeDeletedObjects = $includeDeleted +} + +if ($credential) { + $commonParams.Credential = $credential +} + +if ($domainServer) { + $commonParams.Server = $domainServer +} + +# First get the IDs for all the AD objects that match the filter specified. +$getParams = @{ + Properties = @('DistinguishedName', 'ObjectGUID') +} + +if ($filter) { + $getParams.Filter = $filter +} +elseif ($identity) { + $getParams.Identity = $identity +} +elseif ($ldapFilter) { + $getParams.LDAPFilter = $ldapFilter +} + +# Explicit check on $null as an empty string is different from not being set. +if ($null -ne $searchBase) { + $getParams.SearchBase = $searchbase +} + +if ($searchScope) { + $getParams.SearchScope = switch ($searchScope) { + base { 'Base' } + one_level { 'OneLevel' } + subtree { 'Subtree' } + } +} + +try { + # We run this in a custom PowerShell pipeline so that users of this module can't use any of the variables defined + # above in their filter. While the cmdlet won't execute sub expressions we don't want anyone implicitly relying on + # a defined variable in this module in case we ever change the name or remove it. + $iss = [InitialSessionState]::CreateDefault() + $iss.ImportPSModule("ActiveDirectory") + $ps = [PowerShell]::Create($iss) + $null = $ps.AddCommand('Get-ADObject').AddParameters($commonParams).AddParameters($getParams) + $null = $ps.AddCommand('Select-Object').AddParameter('Property', @('DistinguishedName', 'ObjectGUID')) + + $foundGuids = @($ps.Invoke()) +} +catch { + # Because we ran in a pipeline we can't catch ADIdentityNotFoundException. Instead just get the base exception and + # do the error checking on that. + if ($_.Exception.GetBaseException() -is [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException]) { + $foundGuids = @() + } + else { + # The exception is from the .Invoke() call, compare on the InnerException which was what was actually raised by + # the pipeline. + $innerException = $_.Exception.InnerException.InnerException + if ($innerException -is [Microsoft.ActiveDirectory.Management.ADServerDownException]) { + # Point users in the direction of the double hop problem as that is what is typically the cause of this. + $msg = "Failed to contact the AD server, this could be caused by the double hop problem over WinRM. " + $msg += "Try using the module with auth as Kerberos with credential delegation or CredSSP, become, or " + $msg += "defining the domain_username and domain_password module parameters." + $module.FailJson($msg, $innerException) + } + else { + throw $innerException + } + } +} + +$getParams = @{} +if ($properties) { + $getParams.Properties = $properties +} +$module.Result.objects = @(foreach ($adId in $foundGuids) { + try { + $adObject = Get-ADObject @commonParams @getParams -Identity $adId.ObjectGUID + } + catch { + $msg = "Failed to retrieve properties for AD Object '$($adId.DistinguishedName)': $($_.Exception.Message)" + $module.Warn($msg) + continue + } + + $propertyNames = $adObject.PropertyNames + $propertyNames += ($properties | Where-Object { $_ -ne '*' }) + + # Now process each property to an easy to represent string + $filteredObject = [Ordered]@{} + foreach ($name in ($propertyNames | Sort-Object)) { + # In the case of explicit properties that were asked for but weren't set, Get-ADObject won't actually return + # the property so this is a defensive check against that scenario. + if (-not $adObject.PSObject.Properties.Name.Contains($name)) { + $filteredObject.$name = $null + continue + } + + $value = $adObject.$name + if ($value -is [Microsoft.ActiveDirectory.Management.ADPropertyValueCollection]) { + $value = foreach ($v in $value) { + ConvertTo-OutputValue -InputObject $v + } + } + else { + $value = ConvertTo-OutputValue -InputObject $value + } + $filteredObject.$name = $value + + # For these 2 properties, add an _AnsibleFlags attribute which contains the enum strings that are set. + if ($name -eq 'sAMAccountType') { + $enumValue = [Ansible.WinDomainObjectInfo.sAMAccountType]$value + $filteredObject.'sAMAccountType_AnsibleFlags' = $enumValue.ToString() -split ', ' + } + elseif ($name -eq 'userAccountControl') { + $enumValue = [Ansible.WinDomainObjectInfo.UserAccountControl]$value + $filteredObject.'userAccountControl_AnsibleFlags' = $enumValue.ToString() -split ', ' + } + } + + $filteredObject + }) + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_domain_object_info.py b/ansible_collections/community/windows/plugins/modules/win_domain_object_info.py new file mode 100644 index 000000000..c7efac7fe --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_domain_object_info.py @@ -0,0 +1,158 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_domain_object_info +short_description: Gather information an Active Directory object +description: +- Gather information about multiple Active Directory object(s). +options: + domain_password: + description: + - The password for C(domain_username). + type: str + domain_server: + description: + - Specified the Active Directory Domain Services instance to connect to. + - Can be in the form of an FQDN or NetBIOS name. + - If not specified then the value is based on the default domain of the computer running PowerShell. + type: str + domain_username: + description: + - The username to use when interacting with AD. + - If this is not set then the user that is used for authentication will be the connection user. + - Ansible will be unable to use the connection user unless auth is Kerberos with credential delegation or CredSSP, + or become is used on the task. + type: str + filter: + description: + - Specifies a query string using the PowerShell Expression Language syntax. + - This follows the same rules and formatting as the C(-Filter) parameter for the PowerShell AD cmdlets exception + there is no variable substitutions. + - This is mutually exclusive with I(identity) and I(ldap_filter). + type: str + identity: + description: + - Specifies a single Active Directory object by its distinguished name or its object GUID. + - This is mutually exclusive with I(filter) and I(ldap_filter). + - This cannot be used with either the I(search_base) or I(search_scope) options. + type: str + include_deleted: + description: + - Also search for deleted Active Directory objects. + default: no + type: bool + ldap_filter: + description: + - Like I(filter) but this is a tradiitional LDAP query string to filter the objects to return. + - This is mutually exclusive with I(filter) and I(identity). + type: str + properties: + description: + - A list of properties to return. + - If a property is C(*), all properties that have a set value on the AD object will be returned. + - If a property is valid on the object but not set, it is only returned if defined explicitly in this option list. + - The properties C(DistinguishedName), C(Name), C(ObjectClass), and C(ObjectGUID) are always returned. + - Specifying multiple properties can have a performance impact, it is best to only return what is needed. + - If an invalid property is specified then the module will display a warning for each object it is invalid on. + type: list + elements: str + search_base: + description: + - Specify the Active Directory path to search for objects in. + - This cannot be set with I(identity). + - By default the search base is the default naming context of the target AD instance which is the DN returned by + "(Get-ADRootDSE).defaultNamingContext". + type: str + search_scope: + description: + - Specify the scope of when searching for an object in the C(search_base). + - C(base) will limit the search to the base object so the maximum number of objects returned is always one. This + will not search any objects inside a container.. + - C(one_level) will search the current path and any immediate objects in that path. + - C(subtree) will search the current path and all objects of that path recursively. + - This cannot be set with I(identity). + choices: + - base + - one_level + - subtree + type: str +notes: +- The C(sAMAccountType_AnsibleFlags) and C(userAccountControl_AnsibleFlags) return property is something set by the + module itself as an easy way to view what those flags represent. These properties cannot be used as part of the + I(filter) or I(ldap_filter) and are automatically added if those properties were requested. +- This must be run on a host that has the ActiveDirectory powershell module installed. +author: +- Jordan Borean (@jborean93) +''' + +EXAMPLES = r''' +- name: Get all properties for the specified account using its DistinguishedName + community.windows.win_domain_object_info: + identity: CN=Username,CN=Users,DC=domain,DC=com + properties: '*' + +- name: Get the SID for all user accounts as a filter + community.windows.win_domain_object_info: + filter: ObjectClass -eq 'user' -and objectCategory -eq 'Person' + properties: + - objectSid + +- name: Get the SID for all user accounts as a LDAP filter + community.windows.win_domain_object_info: + ldap_filter: (&(objectClass=user)(objectCategory=Person)) + properties: + - objectSid + +- name: Search all computer accounts in a specific path that were added after February 1st + community.windows.win_domain_object_info: + filter: objectClass -eq 'computer' -and whenCreated -gt '20200201000000.0Z' + properties: '*' + search_scope: one_level + search_base: CN=Computers,DC=domain,DC=com +''' + +RETURN = r''' +objects: + description: + - A list of dictionaries that are the Active Directory objects found and the properties requested. + - The dict's keys are the property name and the value is the value for the property. + - All date properties are return in the ISO 8601 format in the UTC timezone. + - All SID properties are returned as a dict with the keys C(Sid) as the SID string and C(Name) as the translated SID + account name. + - All byte properties are returned as a base64 string. + - All security descriptor properties are returned as the SDDL string of that descriptor. + - The properties C(DistinguishedName), C(Name), C(ObjectClass), and C(ObjectGUID) are always returned. + returned: always + type: list + elements: dict + sample: | + [{ + "accountExpires": 0, + "adminCount": 1, + "CanonicalName": "domain.com/Users/Administrator", + "CN": "Administrator", + "Created": "2020-01-13T09:03:22.0000000Z", + "Description": "Built-in account for administering computer/domain", + "DisplayName": null, + "DistinguishedName": "CN=Administrator,CN=Users,DC=domain,DC=com", + "memberOf": [ + "CN=Group Policy Creator Owners,CN=Users,DC=domain,DC=com", + "CN=Domain Admins",CN=Users,DC=domain,DC=com" + ], + "Name": "Administrator", + "nTSecurityDescriptor": "O:DAG:DAD:PAI(A;;LCRPLORC;;;AU)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;SY)(A;;CCDCLCSWRPWPLOCRSDRCWDWO;;;BA)", + "ObjectCategory": "CN=Person,CN=Schema,CN=Configuration,DC=domain,DC=com", + "ObjectClass": "user", + "ObjectGUID": "c8c6569e-4688-4f3c-8462-afc4ff60817b", + "objectSid": { + "Sid": "S-1-5-21-2959096244-3298113601-420842770-500", + "Name": "DOMAIN\Administrator" + }, + "sAMAccountName": "Administrator", + }] +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_domain_ou.ps1 b/ansible_collections/community/windows/plugins/modules/win_domain_ou.ps1 new file mode 100644 index 000000000..ee6cdc45c --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_domain_ou.ps1 @@ -0,0 +1,253 @@ +#!powershell + +# Copyright: (c) 2020 VMware, Inc. All Rights Reserved. +# SPDX-License-Identifier: GPL-3.0-only +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic +#Requires -Module ActiveDirectory + +$spec = @{ + options = @{ + state = @{ type = "str"; choices = @("absent", "present"); default = "present" } + name = @{ type = "str"; required = $true } + protected = @{ type = "bool"; default = $false } + path = @{ type = "str"; required = $false } + filter = @{type = "str"; default = '*' } + recursive = @{ type = "bool"; default = $false } + domain_username = @{ type = "str"; } + domain_password = @{ type = "str"; no_log = $true } + domain_server = @{ type = "str" } + properties = @{ type = "dict" } + } + required_together = @( + , @('domain_password', 'domain_username') + ) + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$extra_args = @{} +$onboard_extra_args = @{} +if ($null -ne $module.Params.domain_username) { + $domain_password = ConvertTo-SecureString $module.Params.domain_password -AsPlainText -Force + $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $module.Params.domain_username, $domain_password + $extra_args.Credential = $credential + $onboard_extra_args.Credential = $credential +} +if ($null -ne $module.Params.domain_server) { + $extra_args.Server = $module.Params.domain_server + $onboard_extra_args.Server = $module.Params.domain_server +} +if ($module.Params.properties.count -ne 0) { + $Properties = New-Object Collections.Generic.List[string] + $module.Params.properties.Keys | Foreach-Object { + $Properties.Add($_) + } + $extra_args.Properties = $Properties +} +else { + $extra_args.Properties = '*' + $Properties = '*' +} + +$extra_args.Filter = $module.Params.filter +$check_mode = $module.CheckMode +$name = $module.Params.name +$protected = $module.Params.protected +$path = $module.Params.path +$state = $module.Params.state +$recursive = $module.Params.recursive + +# setup Dynamic Params +$params = @{} +if ($module.Params.properties.count -ne 0) { + $module.Params.properties.Keys | ForEach-Object { + $params.Add($_, $module.Params.properties.Item($_)) + } +} + +Function Get-SimulatedOu { + Param($Object) + $ou = @{ + Name = $Object.name + DistinguishedName = "OU=$($Object.name),$($Object.path)" + ProtectedFromAccidentalDeletion = $Object.protected + Properties = New-Object Collections.Generic.List[string] + } + $ou.Properties.Add("Name") + $ou.Properties.Add("DistinguishedName") + $ou.Properties.Add("ProtectedFromAccidentalDeletion") + if ($Object.Params.properties.Count -ne 0) { + $Object.Params.properties.Keys | ForEach-Object { + $property = $_ + $module.Result.simulate_property = $property + $ou.Add($property, $Object.Params.properties.Item($property)) + $ou.Properties.Add($property) + } + } + # convert to psobject & return + [PSCustomObject]$ou +} + +Function Get-OuObject { + Param([PSObject]$Object) + $obj = $Object | Select-Object -Property * -ExcludeProperty nTSecurityDescriptor | ConvertTo-Json -Depth 1 | ConvertFrom-Json + return $obj +} + +# attempt import of module +Try { Import-Module ActiveDirectory } +Catch { $module.FailJson("The ActiveDirectory module failed to load properly: $($_.Exception.Message)", $_) } +Try { + $all_ous = Get-ADOrganizationalUnit @extra_args +} +Catch { $module.FailJson("Get-ADOrganizationalUnit failed: $($_.Exception.Message)", $_) } + +# set path if not defined to base domain +if ($null -eq $path) { + if ($($all_ous | Measure-Object | Select-Object -ExpandProperty Count) -eq 1) { + $matched = $all_ous.DistinguishedName -match "DC=.+" + } + elseif ($($all_ous | Measure-Object | Select-Object -ExpandProperty Count) -gt 1) { + $matched = $all_ous[0].DistinguishedName -match "DC=.+" + } + else { + $module.FailJson("Path was null and unable to determine default domain $($_.Exception.Message)") + } + if ($matched) { + $path = $matches.Values[0] + } + else { + $module.FailJson("Unable to find default domain $($_.Exception.Message)") + } +} +$module.Result.path = $path + +# determine if requested OU exist +$current_ou = $false +Try { + $current_ou = $all_ous | Where-Object { + $_.DistinguishedName -eq "OU=$name,$path" } + $module.Diff.before = Get-OuObject -Object $current_ou + $module.Result.ou = Get-OuObject $module.Diff.before +} +Catch { + $module.Diff.before = "" + $current_ou = $false +} + +# determine if ou needs created +if (($state -eq "present") -and (-not $current_ou)) { + $create_ou = $true +} +else { + $create_ou = $false +} + +# determine if ou needs change +$update_ou = $false +if (($state -eq "present") -and ($create_ou -eq $false)) { + if ($module.Params.properties.Count -ne 0) { + $changed_properties = New-Object Collections.Generic.List[hashtable] + $module.Params.properties.Keys | ForEach-Object { + $property = $_ + $current_value = $current_ou.Item($property) + $requested_value = $module.Params.properties.Item($property) + if (-not ($current_value -eq $requested_value) ) { + $changed_properties.Add( + @{ + "Actual_$property" = $current_value + "Requested_$property" = $requested_value + } + ) + } + } + if ($changed_properties.Count -ge 1) { + $update_ou = $true + } + } +} + +if ($state -eq "present") { + # ou does not exist, create object + if ($create_ou) { + $params.Name = $name + $params.Path = $path + Try { + New-ADOrganizationalUnit @params @onboard_extra_args -ProtectedFromAccidentalDeletion $protected -WhatIf:$check_mode + } + Catch { + $module.FailJson("Failed to create organizational unit: $($_.Exception.Message)", $_) + } + $module.Result.changed = $true + if ($check_mode) { + $module.Diff.after = Get-SimulatedOu -Object $module + } + else { + $new_ou = Get-ADOrganizationalUnit @extra_args | Where-Object { + $_.DistinguishedName -eq "OU=$name,$path" + } + $module.Diff.after = Get-OuObject -Object $new_ou + } + } + # ou exists, update object if needed + if ($update_ou) { + Try { + Set-ADOrganizationalUnit -Identity "OU=$name,$path" @params @onboard_extra_args -WhatIf:$check_mode + $module.Result.changed = $true + } + Catch { + $module.FailJson("Failed to update organizational unit: $($_.Exception.Message)", $_) + } + if ($check_mode) { + $module.Diff.after = Get-SimulatedOu -Object $module + } + else { + $new_ou = Get-ADOrganizationalUnit @extra_args | Where-Object { + $_.DistinguishedName -eq "OU=$name,$path" + } + $module.Diff.after = Get-OuObject -Object $new_ou + } + } +} + +if ($state -eq "absent") { + # ou exists, delete object + if ($current_ou) { + Try { + # override protected from accidental deletion + Set-ADOrganizationalUnit -Identity "OU=$name,$path" -ProtectedFromAccidentalDeletion $false @onboard_extra_args -Confirm:$False -WhatIf:$check_mode + $module.Result.changed = $true + } + Catch { + $module.FailJson("Failed to remove ProtectedFromAccidentalDeletion Lock: $($_.Exception.Message)", $_) + } + # check recursive deletion + if ($recursive) { + try { + Remove-ADOrganizationalUnit -Identity "OU=$name,$path" -Confirm:$False -WhatIf:$check_mode -Recursive @onboard_extra_args + $module.Result.changed = $true + $module.Diff.after = "" + $module.Result.ou = "" + } + catch { + $module.FailJson("Failed to recursively Remove-ADOrganizationalUnit $($_.Exception.Message)", $_) + } + } + else { + try { + Remove-ADOrganizationalUnit -Identity "OU=$name,$path" -Confirm:$False -WhatIf:$check_mode @onboard_extra_args + $module.Result.changed = $true + $module.Diff.after = "" + $module.Result.ou = "" + } + Catch { + $module.FailJson("Failed to Remove-ADOrganizationalUnit: $($_.Exception.Message)", $_) + } + } + } +} + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_domain_ou.py b/ansible_collections/community/windows/plugins/modules/win_domain_ou.py new file mode 100644 index 000000000..e144b8373 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_domain_ou.py @@ -0,0 +1,174 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020 VMware, Inc. All Rights Reserved. +# SPDX-License-Identifier: GPL-3.0-only +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_domain_ou +short_description: Manage Active Directory Organizational Units +author: ['Joe Zollo (@joezollo)', 'Larry Lane (@gamethis)'] +version_added: 1.8.0 +requirements: + - This module requires Windows Server 2012 or Newer + - Powershell ActiveDirectory Module +description: + - Manage Active Directory Organizational Units + - Adds, Removes and Modifies Active Directory Organizational Units + - Task should be delegated to a Windows Active Directory Domain Controller +options: + name: + description: + - The name of the Organizational Unit + type: str + required: true + protected: + description: + - Indicates whether to prevent the object from being deleted. When this + I(protected=true), you cannot delete the corresponding object without + changing the value of the property. + type: bool + default: false + path: + description: + - Specifies the X.500 path of the OU or container where the new object is + created. + - defaults to adding ou at base of domain connected to. + type: str + required: false + state: + description: + - Specifies the desired state of the OU. + - When I(state=present) the module will attempt to create the specified + OU if it does not already exist. + - When I(state=absent), the module will remove the specified OU. + - When I(state=absent) and I(recursive=true), the module will remove all + the OU and all child OU's. + type: str + default: present + choices: [ present, absent ] + recursive: + description: + - Removes the OU and any child items it contains. + - You must specify this parameter to remove an OU that is not empty. + type: bool + default: false + domain_server: + description: + - Specifies the Active Directory Domain Services instance to connect to. + - Can be in the form of an FQDN or NetBIOS name. + - If not specified then the value is based on the domain of the computer + running PowerShell. + type: str + domain_username: + description: + - The username to use when interacting with AD. + - If this is not set then the user Ansible used to log in with will be + used instead when using CredSSP or Kerberos with credential delegation. + type: str + domain_password: + type: str + description: + - The password for the domain you are accessing + filter: + type: str + description: filter for lookup of ou. + default: '*' + properties: + type: dict + description: + - Free form dict of properties for the organizational unit. Follows LDAP property names, like C(StreetAddress) or C(PostalCode). +''' + +EXAMPLES = r''' +--- +- name: Ensure OU is present & protected + community.windows.win_domain_ou: + name: AnsibleFest + state: present + +- name: Ensure OU is present & protected + community.windows.win_domain_ou: + name: EUC Users + path: "DC=euc,DC=vmware,DC=lan" + state: present + protected: true + delegate_to: win-ad1.euc.vmware.lab + +- name: Ensure OU is absent + community.windows.win_domain_ou: + name: EUC Users + path: "DC=euc,DC=vmware,DC=lan" + state: absent + delegate_to: win-ad1.euc.vmware.lab + +- name: Ensure OU is present with specific properties + community.windows.win_domain_ou: + name: WS1Users + path: "CN=EUC Users,DC=euc,DC=vmware,DC=lan" + protected: true + properties: + city: Sandy Springs + state: Georgia + StreetAddress: 1155 Perimeter Center West + country: US + description: EUC Business Unit + PostalCode: 30189 + delegate_to: win-ad1.euc.vmware.lab + +- name: Ensure OU updated with new properties + community.windows.win_domain_ou: + name: WS1Users + path: DC=euc,DC=vmware,DC=lan + protected: false + properties: + city: Atlanta + state: Georgia + managedBy: jzollo@vmware.com + delegate_to: win-ad1.euc.vmware.lab +''' + +RETURN = r''' +path: + description: + - Base ou path used by module either when provided I(path=DC=Ansible,DC=Test) or derived by module. + returned: always + type: str + sample: + path: "DC=ansible,DC=test" +ou: + description: + - New/Updated organizational unit parameters + returned: When I(state=present) + type: dict + sample: + AddedProperties: [] + City: "Sandy Springs" + Country: null + DistinguishedName: "OU=VMW Atlanta,DC=ansible,DC=test" + LinkedGroupPolicyObjects: [] + ManagedBy: null + ModifiedProperties: [] + Name: "VMW Atlanta" + ObjectClass: "organizationalUnit" + ObjectGUID: "3e987e30-93ad-4229-8cd0-cff6a91275e4" + PostalCode: null + PropertyCount: 11 + PropertyNames: + City + Country + DistinguishedName + LinkedGroupPolicyObjects + ManagedBy + Name + ObjectClass + ObjectGUID + PostalCode + State + StreetAddress + RemovedProperties: [] + State: "Georgia" + StreetAddress: "1155 Perimeter Center West" +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_domain_user.ps1 b/ansible_collections/community/windows/plugins/modules/win_domain_user.ps1 new file mode 100644 index 000000000..e87c606cf --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_domain_user.ps1 @@ -0,0 +1,564 @@ +#!powershell + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.AccessToken +#AnsibleRequires -CSharpUtil Ansible.Basic + +Function Test-Credential { + param( + [String]$Username, + [String]$Password, + [String]$Domain = $null + ) + if (($Username.ToCharArray()) -contains [char]'@') { + # UserPrincipalName + $Domain = $null # force $Domain to be null, to prevent undefined behaviour, as a domain name is already included in the username + } + elseif (($Username.ToCharArray()) -contains [char]'\') { + # Pre Win2k Account Name + $Domain = ($Username -split '\\')[0] + $Username = ($Username -split '\\', 2)[-1] + } # If no domain provided, so maybe local user, or domain specified separately. + + try { + $handle = [Ansible.AccessToken.TokenUtil]::LogonUser($Username, $Domain, $Password, "Network", "Default") + $handle.Dispose() + return $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 + ) + $failed_codes = @( + 0x0000052E, # ERROR_LOGON_FAILURE + 0x00000532, # ERROR_PASSWORD_EXPIRED + 0x00000773, # ERROR_PASSWORD_MUST_CHANGE + 0x00000533 # ERROR_ACCOUNT_DISABLED + ) + + if ($_.Exception.NativeErrorCode -in $failed_codes) { + return $false + } + elseif ($_.Exception.NativeErrorCode -in $success_codes) { + return $true + } + else { + # an unknown failure, reraise exception + throw $_ + } + } +} + +$spec = @{ + options = @{ + name = @{ type = 'str'; required = $true } + state = @{ + type = "str" + choices = @('present', 'absent', 'query') + default = "present" + } + domain_username = @{ type = 'str' } + domain_password = @{ type = 'str'; no_log = $true } + domain_server = @{ type = 'str' } + groups_action = @{ + type = 'str' + choices = @('add', 'remove', 'replace') + default = 'replace' + } + spn_action = @{ + type = 'str' + choices = @('add', 'remove', 'replace') + default = 'replace' + } + spn = @{ + type = 'list' + elements = 'str' + aliases = @('spns') + } + description = @{ type = 'str' } + password = @{ type = 'str'; no_log = $true } + password_expired = @{ type = 'bool' } + password_never_expires = @{ type = 'bool' } + user_cannot_change_password = @{ type = 'bool' } + account_locked = @{ type = 'bool' } + groups = @{ type = 'list'; elements = 'str' } + groups_missing_behaviour = @{ type = 'str'; choices = "fail", "ignore", "warn"; default = "fail" } + enabled = @{ type = 'bool'; default = $true } + path = @{ type = 'str' } + upn = @{ type = 'str' } + sam_account_name = @{ type = 'str' } + identity = @{ type = 'str' } + firstname = @{ type = 'str' } + surname = @{ type = 'str'; aliases = @('lastname') } + display_name = @{ type = 'str' } + company = @{ type = 'str' } + email = @{ type = 'str' } + street = @{ type = 'str' } + city = @{ type = 'str' } + state_province = @{ type = 'str' } + postal_code = @{ type = 'str' } + country = @{ type = 'str' } + attributes = @{ type = 'dict' } + delegates = @{ + type = 'list' + elements = 'str' + aliases = @('principals_allowed_to_delegate') + } + update_password = @{ + type = 'str' + choices = @('always', 'on_create', 'when_changed') + default = 'always' + } + } + required_together = @( + , @("domain_username", "domain_password") + ) + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) +$check_mode = $module.CheckMode + +$module.Result.created = $false +$module.Result.password_updated = $false + +try { + Import-Module ActiveDirectory +} +catch { + $msg = "Failed to import ActiveDirectory PowerShell module." + $module.FailJson($msg, $_) +} + +# Module control parameters +$state = $module.Params.state +$update_password = $module.Params.update_password +$groups_action = $module.Params.groups_action +$domain_username = $module.Params.domain_username +$domain_password = $module.Params.domain_password +$domain_server = $module.Params.domain_server + +# User account parameters +$name = $module.Params.name +$description = $module.Params.description +$password = $module.Params.password +$password_expired = $module.Params.password_expired +$password_never_expires = $module.Params.password_never_expires +$user_cannot_change_password = $module.Params.user_cannot_change_password +$account_locked = $module.Params.account_locked +$groups = $module.Params.groups +$groups_missing_behaviour = $module.Params.groups_missing_behaviour +$enabled = $module.Params.enabled +$path = $module.Params.path +$upn = $module.Params.upn +$spn = $module.Params.spn +$spn_action = $module.Params.spn_action +$sam_account_name = $module.Params.sam_account_name +$delegates = $module.Params.delegates +$identity = $module.Params.identity + +if ($null -eq $identity) { + $identity = $name +} + +# User informational parameters +$user_info = @{ + GivenName = $module.Params.firstname + Surname = $module.Params.surname + DisplayName = $module.Params.display_name + Company = $module.Params.company + EmailAddress = $module.Params.email + StreetAddress = $module.Params.street + City = $module.Params.city + State = $module.Params.state_province + PostalCode = $module.Params.postal_code + Country = $module.Params.country +} + +# Additional attributes +$attributes = $module.Params.attributes + +# Parameter validation +If ($null -ne $account_locked -and $account_locked) { + $module.FailJson("account_locked must be set to 'no' if provided") +} + +If (($null -ne $password_expired) -and ($null -ne $password_never_expires)) { + $module.FailJson("password_expired and password_never_expires are mutually exclusive but have both been set") +} + +$extra_args = @{} +if ($null -ne $domain_username) { + $domain_password = ConvertTo-SecureString $domain_password -AsPlainText -Force + $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $domain_username, $domain_password + $extra_args.Credential = $credential +} + +if ($null -ne $domain_server) { + $extra_args.Server = $domain_server +} + +Function Get-PrincipalGroup { + Param ($identity, $args_extra) + try { + $groups = Get-ADPrincipalGroupMembership ` + -Identity $identity @args_extra ` + -ErrorAction Stop + } + catch { + $module.Warn("Failed to enumerate user groups but continuing on: $($_.Exception.Message)") + return @() + } + + $result_groups = foreach ($group in $groups) { + $group.DistinguishedName + } + return $result_groups +} + +try { + $user_obj = Get-ADUser ` + -Identity $identity ` + -Properties ('*', 'msDS-PrincipalName') @extra_args + $user_guid = $user_obj.ObjectGUID +} +catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] { + $user_obj = $null + $user_guid = $null +} + +If ($state -eq 'present') { + # If the account does not exist, create it + If (-not $user_obj) { + $create_args = @{} + $create_args.Name = $name + If ($null -ne $path) { + $create_args.Path = $path + } + If ($null -ne $upn) { + $create_args.UserPrincipalName = $upn + $create_args.SamAccountName = $upn.Split('@')[0] + } + If ($null -ne $sam_account_name) { + $create_args.SamAccountName = $sam_account_name + } + if ($null -ne $password) { + $create_args.AccountPassword = ConvertTo-SecureString $password -AsPlainText -Force + } + $user_obj = New-ADUser @create_args -WhatIf:$check_mode -PassThru @extra_args + $user_guid = $user_obj.ObjectGUID + $module.Result.created = $true + $module.Result.changed = $true + If ($check_mode) { + $module.ExitJson() + } + $user_obj = Get-ADUser -Identity $user_guid -Properties ('*', 'msDS-PrincipalName') @extra_args + } + ElseIf ($password) { + # Don't unnecessary check for working credentials. + # Set the password if we need to. + If ($update_password -eq "always") { + $set_new_credentials = $true + } + elseif ($update_password -eq "when_changed") { + $user_identifier = If ($user_obj.UserPrincipalName) { + $user_obj.UserPrincipalName + } + else { + $user_obj.'msDS-PrincipalName' + } + + $set_new_credentials = -not (Test-Credential -Username $user_identifier -Password $password) + } + else { + $set_new_credentials = $false + } + If ($set_new_credentials) { + $secure_password = ConvertTo-SecureString $password -AsPlainText -Force + try { + Set-ADAccountPassword -Identity $user_guid ` + -Reset:$true ` + -Confirm:$false ` + -NewPassword $secure_password ` + -WhatIf:$check_mode @extra_args + } + catch { + $module.FailJson("Failed to set password on account: $($_.Exception.Message)", $_) + } + $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args + $module.Result.password_updated = $true + $module.Result.changed = $true + } + } + + # Configure password policies + If (($null -ne $password_never_expires) -and ($password_never_expires -ne $user_obj.PasswordNeverExpires)) { + Set-ADUser -Identity $user_guid -PasswordNeverExpires $password_never_expires -WhatIf:$check_mode @extra_args + $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args + $module.Result.changed = $true + } + If (($null -ne $password_expired) -and ($password_expired -ne $user_obj.PasswordExpired)) { + Set-ADUser -Identity $user_guid -ChangePasswordAtLogon $password_expired -WhatIf:$check_mode @extra_args + $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args + $module.Result.changed = $true + } + If (($null -ne $user_cannot_change_password) -and ($user_cannot_change_password -ne $user_obj.CannotChangePassword)) { + Set-ADUser -Identity $user_guid -CannotChangePassword $user_cannot_change_password -WhatIf:$check_mode @extra_args + $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args + $module.Result.changed = $true + } + + # Assign other account settings + If (($null -ne $upn) -and ($upn -ne $user_obj.UserPrincipalName)) { + Set-ADUser -Identity $user_guid -UserPrincipalName $upn -WhatIf:$check_mode @extra_args + $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args + $module.Result.changed = $true + } + If (($null -ne $sam_account_name) -and ($sam_account_name -ne $user_obj.SamAccountName)) { + Set-ADUser -Identity $user_guid -SamAccountName $sam_account_name -WhatIf:$check_mode @extra_args + $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args + $module.Result.changed = $true + } + If (($null -ne $description) -and ($description -ne $user_obj.Description)) { + Set-ADUser -Identity $user_guid -description $description -WhatIf:$check_mode @extra_args + $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args + $module.Result.changed = $true + } + If ($enabled -ne $user_obj.Enabled) { + Set-ADUser -Identity $user_guid -Enabled $enabled -WhatIf:$check_mode @extra_args + $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args + $module.Result.changed = $true + } + If ((-not $account_locked) -and ($user_obj.LockedOut -eq $true)) { + Unlock-ADAccount -Identity $user_guid -WhatIf:$check_mode @extra_args + $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args + $module.Result.changed = $true + } + If ($delegates) { + if (Compare-Object $delegates $user_obj.PrincipalsAllowedToDelegateToAccount) { + Set-ADUser -Identity $user_guid -PrincipalsAllowedToDelegateToAccount $delegates + $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args + $module.Result.changed = $true + } + } + + # configure service principal names + if ($null -ne $spn) { + $current_spn = [Array]$user_obj.ServicePrincipalNames + $desired_spn = [Array]$spn + $spn_diff = @() + + # generate a diff + $desired_spn | ForEach-Object { + if ($current_spn -contains $_) { + $spn_diff += $_ + } + } + + try { + switch ($spn_action) { + "add" { + # the current spn list does not have any spn's in the desired list + if (-not $spn_diff) { + Set-ADUser ` + -Identity $user_guid ` + -ServicePrincipalNames @{ Add = $(($spn | ForEach-Object { "$($_)" } )) } ` + -WhatIf:$check_mode @extra_args + $module.Result.changed = $true + } + } + "remove" { + # the current spn list does not have any differences + # that means we can remove the desired list + if ($spn_diff) { + Set-ADUser ` + -Identity $user_guid ` + -ServicePrincipalNames @{ Remove = $(($spn | ForEach-Object { "$($_)" } )) } ` + -WhatIf:$check_mode @extra_args + $module.Result.changed = $true + } + } + "replace" { + # the current and desired spn lists do not match + if (Compare-Object $current_spn $desired_spn) { + Set-ADUser ` + -Identity $user_guid ` + -ServicePrincipalNames @{ Replace = $(($spn | ForEach-Object { "$($_)" } )) } ` + -WhatIf:$check_mode @extra_args + $module.Result.changed = $true + } + } + } + } + catch { + $module.FailJson("Failed to $spn_action SPN(s)", $_) + } + } + + # Set user information + Foreach ($key in $user_info.Keys) { + If ($null -eq $user_info[$key]) { + continue + } + $value = $user_info[$key] + If ($value -ne $user_obj.$key) { + $set_args = $extra_args.Clone() + $set_args.$key = $value + Set-ADUser -Identity $user_guid -WhatIf:$check_mode @set_args + $module.Result.changed = $true + $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args + } + } + + # Set additional attributes + $set_args = $extra_args.Clone() + $run_change = $false + + if ($null -ne $attributes) { + $add_attributes = @{} + $replace_attributes = @{} + foreach ($attribute in $attributes.GetEnumerator()) { + $attribute_name = $attribute.Key + $attribute_value = $attribute.Value + + $valid_property = [bool]($user_obj.PSobject.Properties.name -eq $attribute_name) + if ($valid_property) { + $existing_value = $user_obj.$attribute_name + if ($existing_value -cne $attribute_value) { + $replace_attributes.$attribute_name = $attribute_value + } + } + else { + $add_attributes.$attribute_name = $attribute_value + } + } + if ($add_attributes.Count -gt 0) { + $set_args.Add = $add_attributes + $run_change = $true + } + if ($replace_attributes.Count -gt 0) { + $set_args.Replace = $replace_attributes + $run_change = $true + } + } + + if ($run_change) { + Set-ADUser -Identity $user_guid -WhatIf:$check_mode @set_args + $module.Result.changed = $true + $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args + } + + # Configure group assignment + if ($null -ne $groups) { + $group_list = $groups + + $groups = @( + Foreach ($group in $group_list) { + try { + (Get-ADGroup -Identity $group @extra_args).DistinguishedName + } + catch { + if ($groups_missing_behaviour -eq "fail") { + $module.FailJson("Failed to locate group $($group): $($_.Exception.Message)", $_) + } + elseif ($groups_missing_behaviour -eq "warn") { + $module.Warn("Failed to locate group $($group) but continuing on: $($_.Exception.Message)") + } + } + } + ) + + $assigned_groups = Get-PrincipalGroup $user_guid $extra_args + + switch ($groups_action) { + "add" { + Foreach ($group in $groups) { + If (-not ($assigned_groups -Contains $group)) { + Add-ADGroupMember -Identity $group -Members $user_guid -WhatIf:$check_mode @extra_args + $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args + $module.Result.changed = $true + } + } + } + "remove" { + Foreach ($group in $groups) { + If ($assigned_groups -Contains $group) { + Remove-ADGroupMember -Identity $group -Members $user_guid -Confirm:$false -WhatIf:$check_mode @extra_args + $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args + $module.Result.changed = $true + } + } + } + "replace" { + Foreach ($group in $assigned_groups) { + If (($group -ne $user_obj.PrimaryGroup) -and -not ($groups -Contains $group)) { + Remove-ADGroupMember -Identity $group -Members $user_guid -Confirm:$false -WhatIf:$check_mode @extra_args + $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args + $module.Result.changed = $true + } + } + Foreach ($group in $groups) { + If (-not ($assigned_groups -Contains $group)) { + Add-ADGroupMember -Identity $group -Members $user_guid -WhatIf:$check_mode @extra_args + $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args + $module.Result.changed = $true + } + } + } + } + } +} +elseif ($state -eq 'absent') { + # Ensure user does not exist + If ($user_obj) { + Remove-ADUser $user_obj -Confirm:$false -WhatIf:$check_mode @extra_args + $module.Result.changed = $true + if ($check_mode) { + $module.ExitJson() + } + $user_obj = $null + } +} + +If ($user_obj) { + $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args + $module.Result.name = $user_obj.Name + $module.Result.firstname = $user_obj.GivenName + $module.Result.surname = $user_obj.Surname + $module.Result.display_name = $user_obj.DisplayName + $module.Result.enabled = $user_obj.Enabled + $module.Result.company = $user_obj.Company + $module.Result.street = $user_obj.StreetAddress + $module.Result.email = $user_obj.EmailAddress + $module.Result.city = $user_obj.City + $module.Result.state_province = $user_obj.State + $module.Result.country = $user_obj.Country + $module.Result.postal_code = $user_obj.PostalCode + $module.Result.distinguished_name = $user_obj.DistinguishedName + $module.Result.description = $user_obj.Description + $module.Result.password_expired = $user_obj.PasswordExpired + $module.Result.password_never_expires = $user_obj.PasswordNeverExpires + $module.Result.user_cannot_change_password = $user_obj.CannotChangePassword + $module.Result.account_locked = $user_obj.LockedOut + $module.Result.delegates = $user_obj.PrincipalsAllowedToDelegateToAccount + $module.Result.sid = [string]$user_obj.SID + $module.Result.spn = [Array]$user_obj.ServicePrincipalNames + $module.Result.upn = $user_obj.UserPrincipalName + $module.Result.sam_account_name = $user_obj.SamAccountName + $module.Result.groups = Get-PrincipalGroup $user_guid $extra_args + $module.Result.msg = "User '$name' is present" + $module.Result.state = "present" +} +else { + $module.Result.name = $name + $module.Result.msg = "User '$name' is absent" + $module.Result.state = "absent" +} + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_domain_user.py b/ansible_collections/community/windows/plugins/modules/win_domain_user.py new file mode 100644 index 000000000..aee5efd0a --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_domain_user.py @@ -0,0 +1,477 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_domain_user +short_description: Manages Windows Active Directory user accounts +description: + - Manages Windows Active Directory user accounts. +options: + name: + description: + - Name of the user to create, remove or modify. + type: str + required: true + identity: + description: + - Identity parameter used to find the User in the Active Directory. + - This value can be in the forms C(Distinguished Name), C(objectGUID), + C(objectSid) or C(sAMAccountName). + - Default to C(name) if not set. + type: str + state: + description: + - When C(present), creates or updates the user account. + - When C(absent), removes the user account if it exists. + - When C(query), retrieves the user account details without making any changes. + type: str + choices: [ absent, present, query ] + default: present + enabled: + description: + - C(yes) will enable the user account. + - C(no) will disable the account. + type: bool + default: yes + account_locked: + description: + - C(no) will unlock the user account if locked. + - Note that there is not a way to lock an account as an administrator. + - Accounts are locked due to user actions; as an admin, you may only unlock a locked account. + - If you wish to administratively disable an account, set I(enabled) to C(no). + type: bool + description: + description: + - Description of the user + type: str + groups: + description: + - Adds or removes the user from this list of groups, + depending on the value of I(groups_action). + - To remove all but the Principal Group, set C(groups=<principal group name>) and + I(groups_action=replace). + - Note that users cannot be removed from their principal group (for example, "Domain Users"). + type: list + elements: str + groups_action: + description: + - If C(add), the user is added to each group in I(groups) where not already a member. + - If C(remove), the user is removed from each group in I(groups). + - If C(replace), the user is added as a member of each group in + I(groups) and removed from any other groups. + type: str + choices: [ add, remove, replace ] + default: replace + groups_missing_behaviour: + description: + - Controls what happens when a group specified by C(groups) is an invalid group name. + - C(fail) is the default and will return an error any groups do not exist. + - C(ignore) will ignore any groups that does not exist. + - C(warn) will display a warning for any groups that do not exist but will continue without failing. + type: str + choices: + - fail + - ignore + - warn + default: fail + version_added: 1.10.0 + spn: + description: + - Specifies the service principal name(s) for the account. This parameter sets the + ServicePrincipalNames property of the account. The LDAP display name (ldapDisplayName) + for this property is servicePrincipalName. + type: list + elements: str + aliases: [ spns ] + version_added: 1.10.0 + spn_action: + description: + - If C(add), the SPNs are added to the user. + - If C(remove), the SPNs are removed from the user. + - If C(replace), the defined set of SPN's overwrite the current set of SPNs. + type: str + choices: [ add, remove, replace ] + default: replace + version_added: 1.10.0 + password: + description: + - Optionally set the user's password to this (plain text) value. + - To enable an account - I(enabled) - a password must already be + configured on the account, or you must provide a password here. + type: str + update_password: + description: + - C(always) will always update passwords. + - C(on_create) will only set the password for newly created users. + - C(when_changed) will only set the password when changed. + type: str + choices: [ always, on_create, when_changed ] + default: always + password_expired: + description: + - C(yes) will require the user to change their password at next login. + - C(no) will clear the expired password flag. + - This is mutually exclusive with I(password_never_expires). + type: bool + password_never_expires: + description: + - C(yes) will set the password to never expire. + - C(no) will allow the password to expire. + - This is mutually exclusive with I(password_expired). + type: bool + 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 + firstname: + description: + - Configures the user's first name (given name). + type: str + surname: + description: + - Configures the user's last name (surname). + type: str + aliases: [ lastname ] + display_name: + description: + - Configures the user's display name. + type: str + version_added: 1.12.0 + company: + description: + - Configures the user's company name. + type: str + upn: + description: + - Configures the User Principal Name (UPN) for the account. + - This is not required, but is best practice to configure for modern + versions of Active Directory. + - The format is C(<username>@<domain>). + type: str + sam_account_name: + description: + - Configures the SAM Account Name (C(sAMAccountName)) for the account. + - This is allowed to a maximum of 20 characters due to pre-Windows 2000 restrictions. + - Default to the C(<username>) specified in C(upn) or C(name) if not set. + type: str + version_added: 1.7.0 + email: + description: + - Configures the user's email address. + - This is a record in AD and does not do anything to configure any email + servers or systems. + type: str + street: + description: + - Configures the user's street address. + type: str + city: + description: + - Configures the user's city. + type: str + state_province: + description: + - Configures the user's state or province. + type: str + postal_code: + description: + - Configures the user's postal code / zip code. + type: str + country: + description: + - Configures the user's country code. + - Note that this is a two-character ISO 3166 code. + type: str + path: + description: + - Container or OU for the new user; if you do not specify this, the + user will be placed in the default container for users in the domain. + - Setting the path is only available when a new user is created; + if you specify a path on an existing user, the user's path will not + be updated - you must delete (e.g., C(state=absent)) the user and + then re-add the user with the appropriate path. + type: str + delegates: + description: + - Specifies an array of principal objects. This parameter sets the + msDS-AllowedToActOnBehalfOfOtherIdentity attribute of a computer account + object. + - Must be specified as a distinguished name C(CN=shenetworks,CN=Users,DC=ansible,DC=test) + type: list + elements: str + aliases: [ principals_allowed_to_delegate ] + version_added: 1.10.0 + attributes: + description: + - A dict of custom LDAP attributes to set on the user. + - This can be used to set custom attributes that are not exposed as module + parameters, e.g. C(telephoneNumber). + - See the examples on how to format this parameter. + type: dict + domain_username: + description: + - The username to use when interacting with AD. + - If this is not set then the user Ansible used to log in with will be + used instead when using CredSSP or Kerberos with credential delegation. + type: str + domain_password: + description: + - The password for I(username). + type: str + domain_server: + description: + - Specifies the Active Directory Domain Services instance to connect to. + - Can be in the form of an FQDN or NetBIOS name. + - If not specified then the value is based on the domain of the computer + running PowerShell. + type: str +notes: + - Works with Windows 2012R2 and newer. + - If running on a server that is not a Domain Controller, credential + delegation through CredSSP or Kerberos with delegation must be used or the + I(domain_username), I(domain_password) must be set. + - Note that some individuals have confirmed successful operation on Windows + 2008R2 servers with AD and AD Web Services enabled, but this has not + received the same degree of testing as Windows 2012R2. +seealso: +- module: ansible.windows.win_domain +- module: ansible.windows.win_domain_controller +- module: community.windows.win_domain_computer +- module: community.windows.win_domain_group +- module: ansible.windows.win_domain_membership +- module: ansible.windows.win_user +- module: community.windows.win_user_profile +author: + - Nick Chandler (@nwchandler) + - Joe Zollo (@zollo) +''' + +EXAMPLES = r''' +- name: Ensure user bob is present with address information + community.windows.win_domain_user: + name: bob + firstname: Bob + surname: Smith + display_name: Mr. Bob Smith + company: BobCo + password: B0bP4ssw0rd + state: present + groups: + - Domain Admins + street: 123 4th St. + city: Sometown + state_province: IN + postal_code: 12345 + country: US + attributes: + telephoneNumber: 555-123456 + +- name: Ensure user bob is created and use custom credentials to create the user + community.windows.win_domain_user: + name: bob + firstname: Bob + surname: Smith + password: B0bP4ssw0rd + state: present + domain_username: DOMAIN\admin-account + domain_password: SomePas2w0rd + domain_server: domain@DOMAIN.COM + +- name: Ensure user bob is present in OU ou=test,dc=domain,dc=local + community.windows.win_domain_user: + name: bob + password: B0bP4ssw0rd + state: present + path: ou=test,dc=domain,dc=local + groups: + - Domain Admins + +- name: Ensure user bob is absent + community.windows.win_domain_user: + name: bob + state: absent + +- name: Ensure user has spn's defined + community.windows.win_domain_user: + name: liz.kenyon + spn: + - MSSQLSvc/us99db-svr95:1433 + - MSSQLSvc/us99db-svr95.vmware.com:1433 + +- name: Ensure user has spn added + community.windows.win_domain_user: + name: liz.kenyon + spn_action: add + spn: + - MSSQLSvc/us99db-svr95:2433 + +- name: Ensure user is created with delegates and spn's defined + community.windows.win_domain_user: + name: shmemmmy + password: The3rubberducki33! + state: present + groups: + - Domain Admins + - Enterprise Admins + delegates: + - CN=shenetworks,CN=Users,DC=ansible,DC=test + - CN=mk.ai,CN=Users,DC=ansible,DC=test + - CN=jessiedotjs,CN=Users,DC=ansible,DC=test + spn: + - MSSQLSvc/us99db-svr95:2433 +''' + +RETURN = r''' +account_locked: + description: true if the account is locked + returned: always + type: bool + sample: false +changed: + description: true if the account changed during execution + returned: always + type: bool + sample: false +city: + description: The user city + returned: always + type: str + sample: Indianapolis +company: + description: The user company + returned: always + type: str + sample: RedHat +country: + description: The user country + returned: always + type: str + sample: US +delegates: + description: Principals allowed to delegate + returned: always + type: list + elements: str + sample: + - CN=svc.tech.unicorn,CN=Users,DC=ansible,DC=test + - CN=geoff,CN=Users,DC=ansible,DC=test + version_added: 1.10.0 +description: + description: A description of the account + returned: always + type: str + sample: Server Administrator +display_name: + description: The user display name + returned: always + type: str + sample: Nick Doe +distinguished_name: + description: DN of the user account + returned: always + type: str + sample: CN=nick,OU=test,DC=domain,DC=local +email: + description: The user email address + returned: always + type: str + sample: nick@domain.local +enabled: + description: true if the account is enabled and false if disabled + returned: always + type: str + sample: true +firstname: + description: The user first name + returned: always + type: str + sample: Nick +groups: + description: AD Groups to which the account belongs + returned: always + type: list + sample: [ "Domain Admins", "Domain Users" ] +msg: + description: Summary message of whether the user is present or absent + returned: always + type: str + sample: User nick is present +name: + description: The username on the account + returned: always + type: str + sample: nick +password_expired: + description: true if the account password has expired + returned: always + type: bool + sample: false +password_updated: + description: true if the password changed during this execution + returned: always + type: bool + sample: true +postal_code: + description: The user postal code + returned: always + type: str + sample: 46033 +sid: + description: The SID of the account + returned: always + type: str + sample: S-1-5-21-2752426336-228313920-2202711348-1175 +spn: + description: The service principal names + returned: always + type: list + sample: + - HTTPSvc/ws1intel-svc1 + - HTTPSvc/ws1intel-svc1.vmware.com + version_added: 1.10.0 +state: + description: The state of the user account + returned: always + type: str + sample: present +state_province: + description: The user state or province + returned: always + type: str + sample: IN +street: + description: The user street address + returned: always + type: str + sample: 123 4th St. +surname: + description: The user last name + returned: always + type: str + sample: Doe +upn: + description: The User Principal Name of the account + returned: always + type: str + sample: nick@domain.local +sam_account_name: + description: The SAM Account Name of the account + returned: always + type: str + sample: nick + version_added: 1.7.0 +user_cannot_change_password: + description: true if the user is not allowed to change password + returned: always + type: str + sample: false +created: + description: Whether a user was created + returned: always + type: bool + sample: true +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_dotnet_ngen.ps1 b/ansible_collections/community/windows/plugins/modules/win_dotnet_ngen.ps1 new file mode 100644 index 000000000..50405925c --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_dotnet_ngen.ps1 @@ -0,0 +1,65 @@ +#!powershell + +# Copyright: (c) 2015, Peter Mounce <public@neverrunwithscissors.com> +# 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 + +$ErrorActionPreference = 'Stop' + +$params = Parse-Args $args -supports_check_mode $true +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false + +$result = @{ + changed = $false +} + +Function Invoke-Ngen($architecture = "") { + $cmd = "$($env:windir)\Microsoft.NET\Framework$($architecture)\v4.0.30319\ngen.exe" + + if (Test-Path -LiteralPath $cmd) { + $arguments = "update /force" + if ($check_mode) { + $ngen_result = @{ + rc = 0 + stdout = "check mode output for $cmd $arguments" + } + } + else { + try { + $ngen_result = Run-Command -command "$cmd $arguments" + } + catch { + Fail-Json -obj $result -message "failed to execute '$cmd $arguments': $($_.Exception.Message)" + } + } + $result."dotnet_ngen$($architecture)_update_exit_code" = $ngen_result.rc + $result."dotnet_ngen$($architecture)_update_output" = $ngen_result.stdout + + $arguments = "executeQueuedItems" + if ($check_mode) { + $executed_queued_items = @{ + rc = 0 + stdout = "check mode output for $cmd $arguments" + } + } + else { + try { + $executed_queued_items = Run-Command -command "$cmd $arguments" + } + catch { + Fail-Json -obj $result -message "failed to execute '$cmd $arguments': $($_.Exception.Message)" + } + } + $result."dotnet_ngen$($architecture)_eqi_exit_code" = $executed_queued_items.rc + $result."dotnet_ngen$($architecture)_eqi_output" = $executed_queued_items.stdout + $result.changed = $true + } +} + +Invoke-Ngen +Invoke-Ngen -architecture "64" + +Exit-Json -obj $result diff --git a/ansible_collections/community/windows/plugins/modules/win_dotnet_ngen.py b/ansible_collections/community/windows/plugins/modules/win_dotnet_ngen.py new file mode 100644 index 000000000..d207bafdf --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_dotnet_ngen.py @@ -0,0 +1,80 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2015, Peter Mounce <public@neverrunwithscissors.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_dotnet_ngen +short_description: Runs ngen to recompile DLLs after .NET updates +description: + - After .NET framework is installed/updated, Windows will probably want to recompile things to optimise for the host. + - This happens via scheduled task, usually at some inopportune time. + - This module allows you to run this task on your own schedule, so you incur the CPU hit at some more convenient and controlled time. + - U(https://docs.microsoft.com/en-us/dotnet/framework/tools/ngen-exe-native-image-generator#native-image-service) + - U(http://blogs.msdn.com/b/dotnet/archive/2013/08/06/wondering-why-mscorsvw-exe-has-high-cpu-usage-you-can-speed-it-up.aspx) +options: {} +notes: + - There are in fact two scheduled tasks for ngen but they have no triggers so aren't a problem. + - There's no way to test if they've been completed. + - The stdout is quite likely to be several megabytes. +author: +- Peter Mounce (@petemounce) +''' + +EXAMPLES = r''' +- name: Run ngen tasks + community.windows.win_dotnet_ngen: +''' + +RETURN = r''' +dotnet_ngen_update_exit_code: + description: The exit code after running the 32-bit ngen.exe update /force + command. + returned: 32-bit ngen executable exists + type: int + sample: 0 +dotnet_ngen_update_output: + description: The stdout after running the 32-bit ngen.exe update /force + command. + returned: 32-bit ngen executable exists + type: str + sample: sample output +dotnet_ngen_eqi_exit_code: + description: The exit code after running the 32-bit ngen.exe + executeQueuedItems command. + returned: 32-bit ngen executable exists + type: int + sample: 0 +dotnet_ngen_eqi_output: + description: The stdout after running the 32-bit ngen.exe executeQueuedItems + command. + returned: 32-bit ngen executable exists + type: str + sample: sample output +dotnet_ngen64_update_exit_code: + description: The exit code after running the 64-bit ngen.exe update /force + command. + returned: 64-bit ngen executable exists + type: int + sample: 0 +dotnet_ngen64_update_output: + description: The stdout after running the 64-bit ngen.exe update /force + command. + returned: 64-bit ngen executable exists + type: str + sample: sample output +dotnet_ngen64_eqi_exit_code: + description: The exit code after running the 64-bit ngen.exe + executeQueuedItems command. + returned: 64-bit ngen executable exists + type: int + sample: 0 +dotnet_ngen64_eqi_output: + description: The stdout after running the 64-bit ngen.exe executeQueuedItems + command. + returned: 64-bit ngen executable exists + type: str + sample: sample output +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_eventlog.ps1 b/ansible_collections/community/windows/plugins/modules/win_eventlog.ps1 new file mode 100644 index 000000000..a2388d9b1 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_eventlog.ps1 @@ -0,0 +1,287 @@ +#!powershell + +# Copyright: (c) 2017, Andrew Saraceni <andrew.saraceni@gmail.com> +# 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" + +function Get-EventLogDetail { + <# + .SYNOPSIS + Get details of an event log, sources, and associated attributes. + Used for comparison against passed-in option values to ensure idempotency. + #> + param( + [String]$LogName + ) + + $log_details = @{} + $log_details.name = $LogName + $log_details.exists = $false + $log = Get-EventLog -List | Where-Object { $_.Log -eq $LogName } + + if ($log) { + $log_details.exists = $true + $log_details.maximum_size_kb = $log.MaximumKilobytes + $log_details.overflow_action = $log.OverflowAction.ToString() + $log_details.retention_days = $log.MinimumRetentionDays + $log_details.entries = $log.Entries.Count + $log_details.sources = [Ordered]@{} + + # Retrieve existing sources and category/message/parameter file locations + # Associating file locations and sources with logs can only be done from the registry + + $root_key = "HKLM:\SYSTEM\CurrentControlSet\Services\EventLog\{0}" -f $LogName + $log_root = Get-ChildItem -LiteralPath $root_key + + foreach ($child in $log_root) { + $source_name = $child.PSChildName + $log_details.sources.$source_name = @{} + $hash_cursor = $log_details.sources.$source_name + + $source_root = "{0}\{1}" -f $root_key, $source_name + $resource_files = Get-ItemProperty -LiteralPath $source_root + + $hash_cursor.category_file = $resource_files.CategoryMessageFile + $hash_cursor.message_file = $resource_files.EventMessageFile + $hash_cursor.parameter_file = $resource_files.ParameterMessageFile + } + } + + return $log_details +} + +function Test-SourceExistence { + <# + .SYNOPSIS + Get information on a source's existence. + Examine existence regarding the parent log it belongs to and its expected state. + #> + param( + [String]$LogName, + [String]$SourceName, + [Switch]$NoLogShouldExist + ) + + $source_exists = [System.Diagnostics.EventLog]::SourceExists($SourceName) + + if ($source_exists -and $NoLogShouldExist) { + Fail-Json -obj $result -message "Source $SourceName already exists and cannot be created" + } + elseif ($source_exists) { + $source_log = [System.Diagnostics.EventLog]::LogNameFromSourceName($SourceName, ".") + if ($source_log -ne $LogName) { + Fail-Json -obj $result -message "Source $SourceName does not belong to log $LogName and cannot be modified" + } + } + + return $source_exists +} + +function ConvertTo-MaximumSize { + <# + .SYNOPSIS + Convert a string KB/MB/GB value to common bytes and KB representations. + .NOTES + Size must be between 64KB and 4GB and divisible by 64KB, as per the MaximumSize parameter of Limit-EventLog. + #> + param( + [String]$Size + ) + + $parsed_size = @{ + bytes = $null + KB = $null + } + + $size_regex = "^\d+(\.\d+)?(KB|MB|GB)$" + if ($Size -notmatch $size_regex) { + Fail-Json -obj $result -message "Maximum size $Size is not properly specified" + } + + $size_upper = $Size.ToUpper() + $size_numeric = [Double]$Size.Substring(0, $Size.Length - 2) + + if ($size_upper.EndsWith("GB")) { + $size_bytes = $size_numeric * 1GB + } + elseif ($size_upper.EndsWith("MB")) { + $size_bytes = $size_numeric * 1MB + } + elseif ($size_upper.EndsWith("KB")) { + $size_bytes = $size_numeric * 1KB + } + + if (($size_bytes -lt 64KB) -or ($size_bytes -ge 4GB)) { + Fail-Json -obj $result -message "Maximum size must be between 64KB and 4GB" + } + elseif (($size_bytes % 64KB) -ne 0) { + Fail-Json -obj $result -message "Maximum size must be divisible by 64KB" + } + + $parsed_size.bytes = $size_bytes + $parsed_size.KB = $size_bytes / 1KB + return $parsed_size +} + +$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 "str" -failifempty $true +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present", "clear", "absent" +$sources = Get-AnsibleParam -obj $params -name "sources" -type "list" +$category_file = Get-AnsibleParam -obj $params -name "category_file" -type "path" +$message_file = Get-AnsibleParam -obj $params -name "message_file" -type "path" +$parameter_file = Get-AnsibleParam -obj $params -name "parameter_file" -type "path" +$maximum_size = Get-AnsibleParam -obj $params -name "maximum_size" -type "str" +$overflow_action = Get-AnsibleParam -obj $params -name "overflow_action" -type "str" -validateset "OverwriteOlder", "OverwriteAsNeeded", "DoNotOverwrite" +$retention_days = Get-AnsibleParam -obj $params -name "retention_days" -type "int" + +$result = @{ + changed = $false + name = $name + sources_changed = @() +} + +$log_details = Get-EventLogDetail -LogName $name + +# Handle common error cases up front +if ($state -eq "present" -and !$log_details.exists -and !$sources) { + # When creating a log, one or more sources must be passed + Fail-Json -obj $result -message "You must specify one or more sources when creating a log for the first time" +} +elseif ($state -eq "present" -and $log_details.exists -and $name -in $sources -and ($category_file -or $message_file -or $parameter_file)) { + # After a default source of the same name is created, it cannot be modified without removing the log + Fail-Json -obj $result -message "Cannot modify default source $name of log $name - you must remove the log" +} +elseif ($state -eq "clear" -and !$log_details.exists) { + Fail-Json -obj $result -message "Cannot clear log $name as it does not exist" +} +elseif ($state -eq "absent" -and $name -in $sources) { + # You also cannot remove a default source for the log - you must remove the log itself + Fail-Json -obj $result -message "Cannot remove default source $name from log $name - you must remove the log" +} + +try { + switch ($state) { + "present" { + foreach ($source in $sources) { + if ($log_details.exists) { + $source_exists = Test-SourceExistence -LogName $name -SourceName $source + } + else { + $source_exists = Test-SourceExistence -LogName $name -SourceName $source -NoLogShouldExist + } + + if ($source_exists) { + $category_change = $category_file -and $log_details.sources.$source.category_file -ne $category_file + $message_change = $message_file -and $log_details.sources.$source.message_file -ne $message_file + $parameter_change = $parameter_file -and $log_details.sources.$source.parameter_file -ne $parameter_file + # Remove source and recreate later if any of the above are true + if ($category_change -or $message_change -or $parameter_change) { + Remove-EventLog -Source $source -WhatIf:$check_mode + } + else { + continue + } + } + + $new_params = @{ + LogName = $name + Source = $source + } + if ($category_file) { + $new_params.CategoryResourceFile = $category_file + } + if ($message_file) { + $new_params.MessageResourceFile = $message_file + } + if ($parameter_file) { + $new_params.ParameterResourceFile = $parameter_file + } + + if (!$check_mode) { + New-EventLog @new_params + $result.sources_changed += $source + } + $result.changed = $true + } + + if ($maximum_size) { + $converted_size = ConvertTo-MaximumSize -Size $maximum_size + } + + $size_change = $maximum_size -and $log_details.maximum_size_kb -ne $converted_size.KB + $overflow_change = $overflow_action -and $log_details.overflow_action -ne $overflow_action + $retention_change = $retention_days -and $log_details.retention_days -ne $retention_days + + if ($size_change -or $overflow_change -or $retention_change) { + $limit_params = @{ + LogName = $name + WhatIf = $check_mode + } + if ($maximum_size) { + $limit_params.MaximumSize = $converted_size.bytes + } + if ($overflow_action) { + $limit_params.OverflowAction = $overflow_action + } + if ($retention_days) { + $limit_params.RetentionDays = $retention_days + } + + Limit-EventLog @limit_params + $result.changed = $true + } + + } + "clear" { + if ($log_details.entries -gt 0) { + Clear-EventLog -LogName $name -WhatIf:$check_mode + $result.changed = $true + } + } + "absent" { + if ($sources -and $log_details.exists) { + # Since sources were passed, remove sources tied to event log + foreach ($source in $sources) { + $source_exists = Test-SourceExistence -LogName $name -SourceName $source + if ($source_exists) { + Remove-EventLog -Source $source -WhatIf:$check_mode + if (!$check_mode) { + $result.sources_changed += $source + } + $result.changed = $true + } + } + } + elseif ($log_details.exists) { + # Only name passed, so remove event log itself (which also removes contained sources) + Remove-EventLog -LogName $name -WhatIf:$check_mode + if (!$check_mode) { + $log_details.sources.GetEnumerator() | ForEach-Object { $result.sources_changed += $_.Name } + } + $result.changed = $true + } + } + } +} +catch { + Fail-Json -obj $result -message $_.Exception.Message +} + +$final_log_details = Get-EventLogDetail -LogName $name +foreach ($final_log_detail in $final_log_details.GetEnumerator()) { + if ($final_log_detail.Name -eq "sources") { + $sources = @() + $final_log_detail.Value.GetEnumerator() | ForEach-Object { $sources += $_.Name } + $result.$($final_log_detail.Name) = [Array]$sources + } + else { + $result.$($final_log_detail.Name) = $final_log_detail.Value + } +} + +Exit-Json -obj $result diff --git a/ansible_collections/community/windows/plugins/modules/win_eventlog.py b/ansible_collections/community/windows/plugins/modules/win_eventlog.py new file mode 100644 index 000000000..8cc9b354e --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_eventlog.py @@ -0,0 +1,159 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2017, Andrew Saraceni <andrew.saraceni@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_eventlog +short_description: Manage Windows event logs +description: + - Allows the addition, clearing and removal of local Windows event logs, + and the creation and removal of sources from a given event log. Also + allows the specification of settings per log and source. +options: + name: + description: + - Name of the event log to manage. + type: str + required: yes + state: + description: + - Desired state of the log and/or sources. + - When C(sources) is populated, state is checked for sources. + - When C(sources) is not populated, state is checked for the specified log itself. + - If C(state) is C(clear), event log entries are cleared for the target log. + type: str + choices: [ absent, clear, present ] + default: present + sources: + description: + - A list of one or more sources to ensure are present/absent in the log. + - When C(category_file), C(message_file) and/or C(parameter_file) are specified, + these values are applied across all sources. + type: list + elements: str + category_file: + description: + - For one or more sources specified, the path to a custom category resource file. + type: path + message_file: + description: + - For one or more sources specified, the path to a custom event message resource file. + type: path + parameter_file: + description: + - For one or more sources specified, the path to a custom parameter resource file. + type: path + maximum_size: + description: + - The maximum size of the event log. + - Value must be between 64KB and 4GB, and divisible by 64KB. + - Size can be specified in KB, MB or GB (e.g. 128KB, 16MB, 2.5GB). + type: str + overflow_action: + description: + - The action for the log to take once it reaches its maximum size. + - For C(DoNotOverwrite), all existing entries are kept and new entries are not retained. + - For C(OverwriteAsNeeded), each new entry overwrites the oldest entry. + - For C(OverwriteOlder), new log entries overwrite those older than the C(retention_days) value. + type: str + choices: [ DoNotOverwrite, OverwriteAsNeeded, OverwriteOlder ] + retention_days: + description: + - The minimum number of days event entries must remain in the log. + - This option is only used when C(overflow_action) is C(OverwriteOlder). + type: int +seealso: +- module: community.windows.win_eventlog_entry +author: + - Andrew Saraceni (@andrewsaraceni) +''' + +EXAMPLES = r''' +- name: Add a new event log with two custom sources + community.windows.win_eventlog: + name: MyNewLog + sources: + - NewLogSource1 + - NewLogSource2 + state: present + +- name: Change the category and message resource files used for NewLogSource1 + community.windows.win_eventlog: + name: MyNewLog + sources: + - NewLogSource1 + category_file: C:\NewApp\CustomCategories.dll + message_file: C:\NewApp\CustomMessages.dll + state: present + +- name: Change the maximum size and overflow action for MyNewLog + community.windows.win_eventlog: + name: MyNewLog + maximum_size: 16MB + overflow_action: DoNotOverwrite + state: present + +- name: Clear event entries for MyNewLog + community.windows.win_eventlog: + name: MyNewLog + state: clear + +- name: Remove NewLogSource2 from MyNewLog + community.windows.win_eventlog: + name: MyNewLog + sources: + - NewLogSource2 + state: absent + +- name: Remove MyNewLog and all remaining sources + community.windows.win_eventlog: + name: MyNewLog + state: absent +''' + +RETURN = r''' +name: + description: The name of the event log. + returned: always + type: str + sample: MyNewLog +exists: + description: Whether the event log exists or not. + returned: success + type: bool + sample: true +entries: + description: The count of entries present in the event log. + returned: success + type: int + sample: 50 +maximum_size_kb: + description: Maximum size of the log in KB. + returned: success + type: int + sample: 512 +overflow_action: + description: The action the log takes once it reaches its maximum size. + returned: success + type: str + sample: OverwriteOlder +retention_days: + description: The minimum number of days entries are retained in the log. + returned: success + type: int + sample: 7 +sources: + description: A list of the current sources for the log. + returned: success + type: list + sample: ["MyNewLog", "NewLogSource1", "NewLogSource2"] +sources_changed: + description: A list of sources changed (e.g. re/created, removed) for the log; + this is empty if no sources are changed. + returned: always + type: list + sample: ["NewLogSource2"] +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_eventlog_entry.ps1 b/ansible_collections/community/windows/plugins/modules/win_eventlog_entry.ps1 new file mode 100644 index 000000000..3934ecf3f --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_eventlog_entry.ps1 @@ -0,0 +1,106 @@ +#!powershell + +# Copyright: (c) 2017, Andrew Saraceni <andrew.saraceni@gmail.com> +# 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" + +function Test-LogExistence { + <# + .SYNOPSIS + Get information on a log's existence. + #> + param( + [String]$LogName + ) + + $log_exists = $false + $log = Get-EventLog -List | Where-Object { $_.Log -eq $LogName } + if ($log) { + $log_exists = $true + } + return $log_exists +} + +function Test-SourceExistence { + <# + .SYNOPSIS + Get information on a source's existence. + #> + param( + [String]$LogName, + [String]$SourceName + ) + + $source_exists = [System.Diagnostics.EventLog]::SourceExists($SourceName) + + if ($source_exists) { + $source_log = [System.Diagnostics.EventLog]::LogNameFromSourceName($SourceName, ".") + if ($source_log -ne $LogName) { + Fail-Json -obj $result -message "Source $SourceName does not belong to log $LogName and cannot be written to" + } + } + + return $source_exists +} + +$params = Parse-Args $args -supports_check_mode $true +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false + +$log = Get-AnsibleParam -obj $params -name "log" -type "str" -failifempty $true +$source = Get-AnsibleParam -obj $params -name "source" -type "str" -failifempty $true +$event_id = Get-AnsibleParam -obj $params -name "event_id" -type "int" -failifempty $true +$message = Get-AnsibleParam -obj $params -name "message" -type "str" -failifempty $true +$entry_type = Get-AnsibleParam -obj $params -name "entry_type" -type "str" -validateset "Error", "FailureAudit", "Information", "SuccessAudit", "Warning" +$category = Get-AnsibleParam -obj $params -name "category" -type "int" +$raw_data = Get-AnsibleParam -obj $params -name "raw_data" -type "str" + +$result = @{ + changed = $false +} + +$log_exists = Test-LogExistence -LogName $log +if (!$log_exists) { + Fail-Json -obj $result -message "Log $log does not exist and cannot be written to" +} + +$source_exists = Test-SourceExistence -LogName $log -SourceName $source +if (!$source_exists) { + Fail-Json -obj $result -message "Source $source does not exist" +} + +if ($event_id -lt 0 -or $event_id -gt 65535) { + Fail-Json -obj $result -message "Event ID must be between 0 and 65535" +} + +$write_params = @{ + LogName = $log + Source = $source + EventId = $event_id + Message = $message +} + +try { + if ($entry_type) { + $write_params.EntryType = $entry_type + } + if ($category) { + $write_params.Category = $category + } + if ($raw_data) { + $write_params.RawData = [Byte[]]($raw_data -split ",") + } + + if (!$check_mode) { + Write-EventLog @write_params + } + $result.changed = $true + $result.msg = "Entry added to log $log from source $source" +} +catch { + Fail-Json -obj $result -message $_.Exception.Message +} + +Exit-Json -obj $result diff --git a/ansible_collections/community/windows/plugins/modules/win_eventlog_entry.py b/ansible_collections/community/windows/plugins/modules/win_eventlog_entry.py new file mode 100644 index 000000000..2d474d585 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_eventlog_entry.py @@ -0,0 +1,78 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2017, Andrew Saraceni <andrew.saraceni@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_eventlog_entry +short_description: Write entries to Windows event logs +description: + - Write log entries to a given event log from a specified source. +options: + log: + description: + - Name of the event log to write an entry to. + type: str + required: yes + source: + description: + - Name of the log source to indicate where the entry is from. + type: str + required: yes + event_id: + description: + - The numeric event identifier for the entry. + - Value must be between 0 and 65535. + type: int + required: yes + message: + description: + - The message for the given log entry. + type: str + required: yes + entry_type: + description: + - Indicates the entry being written to the log is of a specific type. + type: str + choices: [ Error, FailureAudit, Information, SuccessAudit, Warning ] + category: + description: + - A numeric task category associated with the category message file for the log source. + type: int + raw_data: + description: + - Binary data associated with the log entry. + - Value must be a comma-separated array of 8-bit unsigned integers (0 to 255). + type: str +notes: + - This module will always report a change when writing an event entry. +seealso: +- module: community.windows.win_eventlog +author: + - Andrew Saraceni (@andrewsaraceni) +''' + +EXAMPLES = r''' +- name: Write an entry to a Windows event log + community.windows.win_eventlog_entry: + log: MyNewLog + source: NewLogSource1 + event_id: 1234 + message: This is a test log entry. + +- name: Write another entry to a different Windows event log + community.windows.win_eventlog_entry: + log: AnotherLog + source: MyAppSource + event_id: 5000 + message: An error has occurred. + entry_type: Error + category: 5 + raw_data: 10,20 +''' + +RETURN = r''' +# Default return values +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_feature_info.ps1 b/ansible_collections/community/windows/plugins/modules/win_feature_info.ps1 new file mode 100644 index 000000000..4942a5753 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_feature_info.ps1 @@ -0,0 +1,45 @@ +#!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 = @{ + name = @{ type = "str"; default = '*' } + } + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$name = $module.Params.name + +$module.Result.exists = $false + +$features = Get-WindowsFeature -Name $name + +$module.Result.features = @(foreach ($feature in ($features)) { + # These should closely reflect the options for win_feature + @{ + name = $feature.Name + display_name = $feature.DisplayName + description = $feature.Description + installed = $feature.Installed + install_state = $feature.InstallState.ToString() + feature_type = $feature.FeatureType + path = $feature.Path + depth = $feature.Depth + depends_on = $feature.DependsOn + parent = $feature.Parent + server_component_descriptor = $feature.ServerComponentDescriptor + sub_features = $feature.SubFeatures + system_service = $feature.SystemService + best_practices_model_id = $feature.BestPracticesModelId + event_query = $feature.EventQuery + post_configuration_needed = $feature.PostConfigurationNeeded + additional_info = $feature.AdditionalInfo + } + $module.Result.exists = $true + }) + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_feature_info.py b/ansible_collections/community/windows/plugins/modules/win_feature_info.py new file mode 100644 index 000000000..c35d8c361 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_feature_info.py @@ -0,0 +1,166 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_feature_info +version_added: '1.4.0' +short_description: Gather information about Windows features +description: +- Gather information about all or a specific installed Windows feature(s). +options: + name: + description: + - If specified, this is used to match the C(name) of the Windows feature to get the info for. + - Can be a wildcard to match multiple features but the wildcard will only be matched on the C(name) of the feature. + - If omitted then all features will returned. + type: str + default: '*' +seealso: +- module: ansible.windows.win_feature +author: +- Larry Lane (@gamethis) +''' + +EXAMPLES = r''' +- name: Get info for all installed features + community.windows.win_feature_info: + register: feature_info +- name: Get info for a single feature + community.windows.win_feature_info: + name: DNS + register: feature_info +- name: Find all features that start with 'FS' + ansible.windows.win_feature_info: + name: FS* +''' + +RETURN = r''' +exists: + description: Whether any features were found based on the criteria specified. + returned: always + type: bool + sample: true +features: + description: + - A list of feature(s) that were found based on the criteria. + - Will be an empty list if no features were found. + returned: always + type: list + elements: dict + contains: + name: + description: + - Name of feature found. + type: str + sample: AD-Certificate + display_name: + description: + - The Display name of feature found. + type: str + sample: Active Directory Certificate Services + description: + description: + - The description of the feature. + type: str + sample: Example description of the Windows feature. + installed: + description: + - Whether the feature by C(name) is installed. + type: bool + sample: false + install_state: + description: + - The Install State of C(name). + - Values will be one of C(Available), C(Removed), C(Installed). + type: str + sample: Installed + feature_type: + description: + - The Feature Type of C(name). + - Values will be one of C(Role), C(Role Service), C(Feature). + type: str + sample: Feature + path: + description: + - The Path of C(name) feature. + type: str + sample: WoW64 Support + depth: + description: + - Depth of C(name) feature. + type: int + sample: 1 + depends_on: + description: + - The command line that will be run when a C(run_command) failure action is fired. + type: list + elements: str + sample: ['Web-Static-Content', 'Web-Default-Doc'] + parent: + description: + - The parent of feature C(name) if present. + type: str + sample: PowerShellRoot + server_component_descriptor: + description: + - Descriptor of C(name) feature. + type: str + sample: ServerComponent_AD_Certificate + sub_features: + description: + - List of sub features names of feature C(name). + type: list + elements: str + sample: ['WAS-Process-Model', 'WAS-NET-Environment', 'WAS-Config-APIs'] + system_service: + description: + - The name of the service installed by feature C(name). + type: list + elements: str + sample: ['iisadmin', 'w3svc'] + best_practices_model_id: + description: + - BestPracticesModelId for feature C(name). + type: str + sample: Microsoft/Windows/UpdateServices + event_query: + description: + - The EventQuery for feature C(name). + - This will be C(null) if None Present + type: str + sample: IPAMServer.Events.xml + post_configuration_needed: + description: + - Tells if Post Configuration is needed for feature C(name). + type: bool + sample: False + additional_info: + description: + - A list of privileges that the feature requires and will run with + type: dict + contains: + major_version: + description: + - Major Version of feature C(name). + type: int + sample: 8 + minor_version: + description: + - Minor Version of feature C(name). + type: int + sample: 0 + number_id_version: + description: + - Numberic Id of feature C(name). + type: int + sample: 16 + install_name: + description: + - The action to perform once triggered, can be C(start_feature) or C(stop_feature). + type: str + sample: ADCertificateServicesRole +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_file_compression.ps1 b/ansible_collections/community/windows/plugins/modules/win_file_compression.ps1 new file mode 100644 index 000000000..de9059228 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_file_compression.ps1 @@ -0,0 +1,120 @@ +#!powershell + +# Copyright: (c) 2019, Micah Hunsberger +# 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 + +$spec = @{ + options = @{ + path = @{ type = 'path'; required = $true } + state = @{ type = 'str'; default = 'present'; choices = 'absent', 'present' } + recurse = @{ type = 'bool'; default = $false } + force = @{ type = 'bool'; default = $true } + } + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$path = $module.Params.path +$state = $module.Params.state +$recurse = $module.Params.recurse +$force = $module.Params.force + +$module.Result.rc = 0 + +if (-not (Test-Path -LiteralPath $path)) { + $module.FailJson("Path to item, $path, does not exist.") +} + +$item = Get-Item -LiteralPath $path -Force # Use -Force for hidden files +if (-not $item.PSIsContainer -and $recurse) { + $module.Warn("The recurse option has no effect when path is not a folder.") +} + +$cim_params = @{ + ClassName = 'Win32_LogicalDisk' + Filter = "DeviceId='$($item.PSDrive.Name):'" + Property = @('FileSystem', 'SupportsFileBasedCompression') +} +$drive_info = Get-CimInstance @cim_params +if ($drive_info.SupportsFileBasedCompression -eq $false) { + $module.FailJson("Path, $path, is not on a filesystemi '$($drive_info.FileSystem)' that supports file based compression.") +} + +function Get-ReturnCodeMessage { + param( + [int]$code + ) + switch ($code) { + 0 { return "The request was successful." } + 2 { return "Access was denied." } + 8 { return "An unspecified failure occurred." } + 9 { return "The name specified was not valid." } + 10 { return "The object specified already exists." } + 11 { return "The file system is not NTFS." } + 12 { return "The platform is not Windows." } + 13 { return "The drive is not the same." } + 14 { return "The directory is not empty." } + 15 { return "There has been a sharing violation." } + 16 { return "The start file specified was not valid." } + 17 { return "A privilege required for the operation is not held." } + 21 { return "A parameter specified is not valid." } + } +} + +function Get-EscapedFileName { + param( + [string]$FullName + ) + return $FullName.Replace("\", "\\").Replace("'", "\'") +} + +$is_compressed = ($item.Attributes -band [System.IO.FileAttributes]::Compressed) -eq [System.IO.FileAttributes]::Compressed +$needs_changed = $is_compressed -ne ($state -eq 'present') + +if ($force -and $recurse -and $item.PSIsContainer) { + if (-not $needs_changed) { + # Check the subfolders and files + $entries_to_check = $item.EnumerateFileSystemInfos("*", [System.IO.SearchOption]::AllDirectories) + foreach ($entry in $entries_to_check) { + $is_compressed = ($entry.Attributes -band [System.IO.FileAttributes]::Compressed) -eq [System.IO.FileAttributes]::Compressed + if ($is_compressed -ne ($state -eq 'present')) { + $needs_changed = $true + break + } + } + } +} + +if ($needs_changed) { + $module.Result.changed = $true + if ($item.PSIsContainer) { + $cim_obj = Get-CimInstance -ClassName 'Win32_Directory' -Filter "Name='$(Get-EscapedFileName -FullName $item.FullName)'" + } + else { + $cim_obj = Get-CimInstance -ClassName 'CIM_LogicalFile' -Filter "Name='$(Get-EscapedFileName -FullName $item.FullName)'" + } + if ($state -eq 'present') { + if (-not $module.CheckMode) { + $ret = Invoke-CimMethod -InputObject $cim_obj -MethodName 'CompressEx' -Arguments @{ Recursive = $recurse } + $module.Result.rc = $ret.ReturnValue + } + } + else { + if (-not $module.CheckMode) { + $ret = $ret = Invoke-CimMethod -InputObject $cim_obj -MethodName 'UnCompressEx' -Arguments @{ Recursive = $recurse } + $module.Result.rc = $ret.ReturnValue + } + } +} + +$module.Result.msg = Get-ReturnCodeMessage -code $module.Result.rc +if ($module.Result.rc -ne 0) { + $module.FailJson($module.Result.msg) +} + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_file_compression.py b/ansible_collections/community/windows/plugins/modules/win_file_compression.py new file mode 100644 index 000000000..265d4bd8b --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_file_compression.py @@ -0,0 +1,93 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Micah Hunsberger (@mhunsber) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_file_compression +short_description: Alters the compression of files and directories on NTFS partitions. +description: + - This module sets the compressed attribute for files and directories on a filesystem that supports it like NTFS. + - NTFS compression can be used to save disk space. +options: + path: + description: + - The full path of the file or directory to modify. + - The path must exist on file system that supports compression like NTFS. + required: yes + type: path + state: + description: + - Set to C(present) to ensure the I(path) is compressed. + - Set to C(absent) to ensure the I(path) is not compressed. + type: str + choices: + - absent + - present + default: present + recurse: + description: + - Whether to recursively apply changes to all subdirectories and files. + - This option only has an effect when I(path) is a directory. + - When set to C(false), only applies changes to I(path). + - When set to C(true), applies changes to I(path) and all subdirectories and files. + type: bool + default: false + force: + description: + - This option only has an effect when I(recurse) is C(true) + - If C(true), will check the compressed state of all subdirectories and files + and make a change if any are different from I(compressed). + - If C(false), will only make a change if the compressed state of I(path) is different from I(compressed). + - If the folder structure is complex or contains a lot of files, it is recommended to set this + option to C(false) so that not every file has to be checked. + type: bool + default: true +author: + - Micah Hunsberger (@mhunsber) +notes: + - M(community.windows.win_file_compression) sets the file system's compression state, it does not create a zip + archive file. + - For more about NTFS Compression, see U(http://www.ntfs.com/ntfs-compressed.htm) +''' + +EXAMPLES = r''' +- name: Compress log files directory + community.windows.win_file_compression: + path: C:\Logs + state: present + +- name: Decompress log files directory + community.windows.win_file_compression: + path: C:\Logs + state: absent + +- name: Compress reports directory and all subdirectories + community.windows.win_file_compression: + path: C:\business\reports + state: present + recurse: yes + +# This will only check C:\business\reports for the compressed state +# If C:\business\reports is compressed, it will not make a change +# even if one of the child items is uncompressed + +- name: Compress reports directory and all subdirectories (quick) + community.windows.win_file_compression: + path: C:\business\reports + compressed: yes + recurse: yes + force: no +''' + +RETURN = r''' +rc: + description: + - The return code of the compress/uncompress operation. + - If no changes are made or the operation is successful, rc is 0. + returned: always + sample: 0 + type: int +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_file_version.ps1 b/ansible_collections/community/windows/plugins/modules/win_file_version.ps1 new file mode 100644 index 000000000..8ca94e81a --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_file_version.ps1 @@ -0,0 +1,63 @@ +#!powershell + +# Copyright: (c) 2015, Sam Liu <sam.liu@activenetwork.com> +# 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 + +$result = @{ + win_file_version = @{} + changed = $false +} + +$path = Get-AnsibleParam -obj $params -name "path" -type "path" -failifempty $true -resultobj $result + +If (-Not (Test-Path -LiteralPath $path -PathType Leaf)) { + Fail-Json $result "Specified path $path does not exist or is not a file." +} +$ext = [System.IO.Path]::GetExtension($path) +If ( $ext -notin '.exe', '.dll') { + Fail-Json $result "Specified path $path is not a valid file type; must be DLL or EXE." +} + +Try { + $_version_fields = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($path) + $file_version = $_version_fields.FileVersion + If ($null -eq $file_version) { + $file_version = '' + } + $product_version = $_version_fields.ProductVersion + If ($null -eq $product_version) { + $product_version = '' + } + $file_major_part = $_version_fields.FileMajorPart + If ($null -eq $file_major_part) { + $file_major_part = '' + } + $file_minor_part = $_version_fields.FileMinorPart + If ($null -eq $file_minor_part) { + $file_minor_part = '' + } + $file_build_part = $_version_fields.FileBuildPart + If ($null -eq $file_build_part) { + $file_build_part = '' + } + $file_private_part = $_version_fields.FilePrivatePart + If ($null -eq $file_private_part) { + $file_private_part = '' + } +} +Catch { + Fail-Json $result "Error: $_.Exception.Message" +} + +$result.win_file_version.path = $path.toString() +$result.win_file_version.file_version = $file_version.toString() +$result.win_file_version.product_version = $product_version.toString() +$result.win_file_version.file_major_part = $file_major_part.toString() +$result.win_file_version.file_minor_part = $file_minor_part.toString() +$result.win_file_version.file_build_part = $file_build_part.toString() +$result.win_file_version.file_private_part = $file_private_part.toString() +Exit-Json $result diff --git a/ansible_collections/community/windows/plugins/modules/win_file_version.py b/ansible_collections/community/windows/plugins/modules/win_file_version.py new file mode 100644 index 000000000..15fec88d5 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_file_version.py @@ -0,0 +1,73 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2015, Sam Liu <sam.liu@activenetwork.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_file_version +short_description: Get DLL or EXE file build version +description: + - Get DLL or EXE file build version. +notes: + - This module will always return no change. +options: + path: + description: + - File to get version. + - Always provide absolute path. + type: path + required: yes +seealso: +- module: ansible.windows.win_file +author: +- Sam Liu (@SamLiu79) +''' + +EXAMPLES = r''' +- name: Get acm instance version + community.windows.win_file_version: + path: C:\Windows\System32\cmd.exe + register: exe_file_version + +- debug: + msg: '{{ exe_file_version }}' +''' + +RETURN = r''' +path: + description: file path + returned: always + type: str + +file_version: + description: File version number.. + returned: no error + type: str + +product_version: + description: The version of the product this file is distributed with. + returned: no error + type: str + +file_major_part: + description: the major part of the version number. + returned: no error + type: str + +file_minor_part: + description: the minor part of the version number of the file. + returned: no error + type: str + +file_build_part: + description: build number of the file. + returned: no error + type: str + +file_private_part: + description: file private part number. + returned: no error + type: str +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_firewall.ps1 b/ansible_collections/community/windows/plugins/modules/win_firewall.ps1 new file mode 100644 index 000000000..9a2039866 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_firewall.ps1 @@ -0,0 +1,90 @@ +#!powershell + +# Copyright: (c) 2017, Michael Eaton <meaton@iforium.com> +# 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" +$firewall_profiles = @('Domain', 'Private', 'Public') + +$params = Parse-Args $args -supports_check_mode $true +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false + +$profiles = Get-AnsibleParam -obj $params -name "profiles" -type "list" -default @("Domain", "Private", "Public") +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -failifempty $true -validateset 'disabled', 'enabled' +$inbound_action = Get-AnsibleParam -obj $params -name "inbound_action" -type "str" -validateset 'allow', 'block', 'not_configured' +$outbound_action = Get-AnsibleParam -obj $params -name "outbound_action" -type "str" -validateset 'allow', 'block', 'not_configured' + +$result = @{ + changed = $false + profiles = $profiles + state = $state +} + +try { + get-command Get-NetFirewallProfile > $null + get-command Set-NetFirewallProfile > $null +} +catch { + Fail-Json $result "win_firewall requires Get-NetFirewallProfile and Set-NetFirewallProfile Cmdlets." +} + +$FIREWALL_ENABLED = [Microsoft.PowerShell.Cmdletization.GeneratedTypes.NetSecurity.GpoBoolean]::True +$FIREWALL_DISABLED = [Microsoft.PowerShell.Cmdletization.GeneratedTypes.NetSecurity.GpoBoolean]::False + +Try { + + ForEach ($profile in $firewall_profiles) { + $current_profile = Get-NetFirewallProfile -Name $profile + $currentstate = $current_profile.Enabled + $current_inboundaction = $current_profile.DefaultInboundAction + $current_outboundaction = $current_profile.DefaultOutboundAction + $result.$profile = @{ + enabled = ($currentstate -eq $FIREWALL_ENABLED) + considered = ($profiles -contains $profile) + currentstate = $currentstate + } + + if ($profiles -notcontains $profile) { + continue + } + + if ($state -eq 'enabled') { + + if ($currentstate -eq $FIREWALL_DISABLED) { + Set-NetFirewallProfile -name $profile -Enabled true -WhatIf:$check_mode + $result.changed = $true + $result.$profile.enabled = $true + } + if ($null -ne $inbound_action) { + $inbound_action = [Globalization.CultureInfo]::InvariantCulture.TextInfo.ToTitleCase($inbound_action.ToLower()) -replace '_', '' + if ($inbound_action -ne $current_inboundaction) { + Set-NetFirewallProfile -name $profile -DefaultInboundAction $inbound_action -WhatIf:$check_mode + $result.changed = $true + } + } + if ($null -ne $outbound_action) { + $outbound_action = [Globalization.CultureInfo]::InvariantCulture.TextInfo.ToTitleCase($outbound_action.ToLower()) -replace '_', '' + if ($outbound_action -ne $current_outboundaction) { + Set-NetFirewallProfile -name $profile -DefaultOutboundAction $outbound_action -WhatIf:$check_mode + $result.changed = $true + } + } + } + else { + + if ($currentstate -eq $FIREWALL_ENABLED) { + Set-NetFirewallProfile -name $profile -Enabled false -WhatIf:$check_mode + $result.changed = $true + $result.$profile.enabled = $false + } + + } + } +} +Catch { + Fail-Json $result "an error occurred when attempting to change firewall status for profile $profile $($_.Exception.Message)" +} + +Exit-Json $result diff --git a/ansible_collections/community/windows/plugins/modules/win_firewall.py b/ansible_collections/community/windows/plugins/modules/win_firewall.py new file mode 100644 index 000000000..ba9f2864d --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_firewall.py @@ -0,0 +1,89 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2017, Michael Eaton <meaton@iforium.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_firewall +short_description: Enable or disable the Windows Firewall +description: +- Enable or Disable Windows Firewall profiles. +requirements: + - This module requires Windows Management Framework 5 or later. +options: + profiles: + description: + - Specify one or more profiles to change. + type: list + elements: str + choices: [ Domain, Private, Public ] + default: [ Domain, Private, Public ] + state: + description: + - Set state of firewall for given profile. + type: str + choices: [ disabled, enabled ] + inbound_action: + description: + - Set to C(allow) or C(block) inbound network traffic in the profile. + - C(not_configured) is valid when configuring a GPO. + type: str + choices: [ allow, block, not_configured ] + version_added: 1.1.0 + outbound_action: + description: + - Set to C(allow) or C(block) inbound network traffic in the profile. + - C(not_configured) is valid when configuring a GPO. + type: str + choices: [ allow, block, not_configured ] + version_added: 1.1.0 +seealso: +- module: community.windows.win_firewall_rule +author: +- Michael Eaton (@michaeldeaton) +''' + +EXAMPLES = r''' +- name: Enable firewall for Domain, Public and Private profiles + community.windows.win_firewall: + state: enabled + profiles: + - Domain + - Private + - Public + tags: enable_firewall + +- name: Disable Domain firewall + community.windows.win_firewall: + state: disabled + profiles: + - Domain + tags: disable_firewall + +- name: Enable firewall for Domain profile and block outbound connections + community.windows.win_firewall: + profiles: Domain + state: enabled + outbound_action: block + tags: block_connection +''' + +RETURN = r''' +enabled: + description: Current firewall status for chosen profile (after any potential change). + returned: always + type: bool + sample: true +profiles: + description: Chosen profile. + returned: always + type: str + sample: Domain +state: + description: Desired state of the given firewall profile(s). + returned: always + type: list + sample: enabled +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_firewall_rule.ps1 b/ansible_collections/community/windows/plugins/modules/win_firewall_rule.ps1 new file mode 100644 index 000000000..91afe3da3 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_firewall_rule.ps1 @@ -0,0 +1,346 @@ +#!powershell + +# Copyright: (c) 2014, Timothy Vandenbrande <timothy.vandenbrande@gmail.com> +# Copyright: (c) 2017, Artem Zinenko <zinenkoartem@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#Requires -Module Ansible.ModuleUtils.Legacy +#AnsibleRequires -PowerShell Ansible.ModuleUtils.AddType + +function ConvertTo-ProtocolType { + param($protocol) + + $protocolNumber = $protocol -as [int] + if ($protocolNumber -is [int]) { + return $protocolNumber + } + + switch -wildcard ($protocol) { + "tcp" { return [System.Net.Sockets.ProtocolType]::Tcp -as [int] } + "udp" { return [System.Net.Sockets.ProtocolType]::Udp -as [int] } + "icmpv4*" { return [System.Net.Sockets.ProtocolType]::Icmp -as [int] } + "icmpv6*" { return [System.Net.Sockets.ProtocolType]::IcmpV6 -as [int] } + default { throw "Unknown protocol '$protocol'." } + } +} + +# See 'Direction' constants here: https://msdn.microsoft.com/en-us/library/windows/desktop/aa364724(v=vs.85).aspx +function ConvertTo-Direction { + param($directionStr) + + switch ($directionStr) { + "in" { return 1 } + "out" { return 2 } + default { throw "Unknown direction '$directionStr'." } + } +} + +# See 'Action' constants here: https://msdn.microsoft.com/en-us/library/windows/desktop/aa364724(v=vs.85).aspx +function ConvertTo-Action { + param($actionStr) + + switch ($actionStr) { + "block" { return 0 } + "allow" { return 1 } + default { throw "Unknown action '$actionStr'." } + } +} + +# Profile enum values: https://msdn.microsoft.com/en-us/library/windows/desktop/aa366303(v=vs.85).aspx +function ConvertTo-Profile { + param($profilesList) + + $profiles = ($profilesList | Select-Object -Unique | ForEach-Object { + switch ($_) { + "domain" { return 1 } + "private" { return 2 } + "public" { return 4 } + default { throw "Unknown profile '$_'." } + } + } | Measure-Object -Sum).Sum + + if ($profiles -eq 7) { return 0x7fffffff } + return $profiles +} + +function ConvertTo-InterfaceType { + param($interfaceTypes) + + return ($interfaceTypes | Select-Object -Unique | ForEach-Object { + switch ($_) { + "wireless" { return "Wireless" } + "lan" { return "Lan" } + "ras" { return "RemoteAccess" } + default { throw "Unknown interface type '$_'." } + } + }) -Join "," +} + +function ConvertTo-EdgeTraversalOption { + param($edgeTraversalOptionsStr) + + switch ($edgeTraversalOptionsStr) { + "yes" { return 1 } + "deferapp" { return 2 } + "deferuser" { return 3 } + default { throw "Unknown edge traversal options '$edgeTraversalOptionsStr'." } + } +} + +function ConvertTo-SecureFlag { + param($secureFlagsStr) + + switch ($secureFlagsStr) { + "authnoencap" { return 1 } + "authenticate" { return 2 } + "authdynenc" { return 3 } + "authenc" { return 4 } + default { throw "Unknown secure flags '$secureFlagsStr'." } + } +} + +$ErrorActionPreference = "Stop" + +$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 +$diff_support = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false + +$name = Get-AnsibleParam -obj $params -name "name" +$description = Get-AnsibleParam -obj $params -name "description" -type "str" +$direction = Get-AnsibleParam -obj $params -name "direction" -type "str" -validateset "in", "out" +$action = Get-AnsibleParam -obj $params -name "action" -type "str" -validateset "allow", "block" +$program = Get-AnsibleParam -obj $params -name "program" -type "str" +$group = Get-AnsibleParam -obj $params -name "group" -type "str" +$service = Get-AnsibleParam -obj $params -name "service" -type "str" +$enabled = Get-AnsibleParam -obj $params -name "enabled" -type "bool" -aliases "enable" +$profiles = Get-AnsibleParam -obj $params -name "profiles" -type "list" -aliases "profile" +$localip = Get-AnsibleParam -obj $params -name "localip" -type "str" +$remoteip = Get-AnsibleParam -obj $params -name "remoteip" -type "str" +$localport = Get-AnsibleParam -obj $params -name "localport" -type "str" +$remoteport = Get-AnsibleParam -obj $params -name "remoteport" -type "str" +$protocol = Get-AnsibleParam -obj $params -name "protocol" -type "str" +$interfacetypes = Get-AnsibleParam -obj $params -name "interfacetypes" -type "list" +$edge = Get-AnsibleParam -obj $params -name "edge" -type "str" -validateset "no", "yes", "deferapp", "deferuser" +$security_options = "notrequired", "authnoencap", "authenticate", "authdynenc", "authenc" +$security = Get-AnsibleParam -obj $params -name "security" -type "str" -validateset $security_options +$icmp_type_code = Get-AnsibleParam -obj $params -name "icmp_type_code" -type "list" + +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present", "absent" + +$_remote_tmp = Get-AnsibleParam $params "_ansible_remote_tmp" -type "path" -default $env:TMP +Add-CSharpType -TempPath $_remote_tmp -References @' +using System; +using System.Runtime.InteropServices; + +namespace Community.Windows.WinFirewallRule +{ + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)] + [Guid("AF230D27-BABA-4E42-ACED-F524F22CFCE2")] + public interface INetFwRule + { + [DispId(3)] + string ApplicationName { get; set; } + } + + public static class NetFwRule + { + public static void PutApplicationName(object rule, string value) + { + ((INetFwRule)rule).ApplicationName = string.IsNullOrEmpty(value) ? null : value; + } + } +} +'@ + +if (-not $name -and -not $group) { + Fail-Json -obj $result -message "Either name or group must be specified" +} + +if ($diff_support) { + $result.diff = @{} + $result.diff.prepared = "" +} + +if ($null -ne $icmp_type_code) { + # COM representation is just "<type>:<code>,<type2>:<code>" so we just join our list + $icmp_type_code = $icmp_type_code -join "," +} + +try { + $fw = New-Object -ComObject HNetCfg.FwPolicy2 + + # If name was specified, filter the rules by name, otherwise find all the rules in the group. + $existingRules = $fw.Rules | Where-Object { + if ($name) { + $_.Name -eq $name + } + else { + $_.Grouping -eq $group + } + } + + # INetFwRule interface description: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365344(v=vs.85).aspx + $new_rule = New-Object -ComObject HNetCfg.FWRule + if ($name) { + $new_rule.Name = $name + } + # the default for enabled in module description is "true", but the actual COM object defaults to "false" when created + if ($null -ne $enabled) { $new_rule.Enabled = $enabled } else { $new_rule.Enabled = $true } + if ($null -ne $description) { $new_rule.Description = $description } + if ($null -ne $group) { $new_rule.Grouping = $group } + if ($null -ne $program -and $program -ne "any") { $new_rule.ApplicationName = [System.Environment]::ExpandEnvironmentVariables($program) } + if ($null -ne $service -and $service -ne "any") { $new_rule.ServiceName = $service } + if ($null -ne $protocol -and $protocol -ne "any") { $new_rule.Protocol = ConvertTo-ProtocolType -protocol $protocol } + if ($null -ne $localport -and $localport -ne "any") { $new_rule.LocalPorts = $localport } + if ($null -ne $remoteport -and $remoteport -ne "any") { $new_rule.RemotePorts = $remoteport } + if ($null -ne $localip -and $localip -ne "any") { $new_rule.LocalAddresses = $localip } + if ($null -ne $remoteip -and $remoteip -ne "any") { $new_rule.RemoteAddresses = $remoteip } + if ($null -ne $icmp_type_code -and $icmp_type_code -ne "any") { $new_rule.IcmpTypesAndCodes = $icmp_type_code } + if ($null -ne $direction) { $new_rule.Direction = ConvertTo-Direction -directionStr $direction } + if ($null -ne $action) { $new_rule.Action = ConvertTo-Action -actionStr $action } + # Profiles value cannot be a uint32, but the "all profiles" value (0x7FFFFFFF) will often become a uint32, so must cast to [int] + if ($null -ne $profiles) { $new_rule.Profiles = [int](ConvertTo-Profile -profilesList $profiles) } + if ($null -ne $interfacetypes -and @(Compare-Object -ReferenceObject $interfacetypes -DifferenceObject @("any")).Count -ne 0) { + $new_rule.InterfaceTypes = ConvertTo-InterfaceType -interfaceTypes $interfacetypes + } + if ($null -ne $edge -and $edge -ne "no") { + # EdgeTraversalOptions property exists only from Windows 7/Windows Server 2008 R2 + # https://msdn.microsoft.com/en-us/library/windows/desktop/dd607256(v=vs.85).aspx + if ($new_rule | Get-Member -Name 'EdgeTraversalOptions') { + $new_rule.EdgeTraversalOptions = ConvertTo-EdgeTraversalOption -edgeTraversalOptionsStr $edge + } + } + if ($null -ne $security -and $security -ne "notrequired") { + # SecureFlags property exists only from Windows 8/Windows Server 2012 + # https://msdn.microsoft.com/en-us/library/windows/desktop/hh447465(v=vs.85).aspx + if ($new_rule | Get-Member -Name 'SecureFlags') { + $new_rule.SecureFlags = ConvertTo-SecureFlag -secureFlagsStr $security + } + } + + $fwPropertiesToCompare = @('Description', 'Direction', 'Action', 'ApplicationName', 'Grouping', 'ServiceName', 'Enabled', + 'Profiles', 'LocalAddresses', 'RemoteAddresses', 'LocalPorts', 'RemotePorts', 'Protocol', 'InterfaceTypes', + 'EdgeTraversalOptions', 'SecureFlags', 'IcmpTypesAndCodes') + $userPassedArguments = @($description, $direction, $action, $program, $group, $service, $enabled, $profiles, $localip, + $remoteip, $localport, $remoteport, $protocol, $interfacetypes, $edge, $security, $icmp_type_code) + + if ($state -eq "absent") { + if (-not $existingRules) { + if ($name) { + $result.msg = "Firewall rule '$name' does not exist." + } + else { + $result.msg = "No firewall rules in group '$group' exist." + } + + } + else { + $rules = foreach ($rule in $existingRules) { + $rule.Name # Output name for module msg string. + + if ($diff_support) { + $result.diff.prepared += "-[$($rule.Name)]`n" + foreach ($prop in $fwPropertiesToCompare) { + $result.diff.prepared += "-$($prop)='$($rule.$prop)'`n" + } + } + + if (-not $check_mode) { + $fw.Rules.Remove($rule.Name) + } + $result.changed = $true + } + $result.msg = "Firewall rule(s) '$($rules -join "', '")' removed." + } + } + elseif ($state -eq "present") { + if (-not $existingRules -and $name) { + # name was specified and no rules were found, create the rule + if ($diff_support) { + $result.diff.prepared += "+[$($new_rule.Name)]`n" + foreach ($prop in $fwPropertiesToCompare) { + $result.diff.prepared += "+$($prop)='$($new_rule.$prop)'`n" + } + } + + if (-not $check_mode) { + $fw.Rules.Add($new_rule) + } + $result.changed = $true + $result.msg = "Firewall rule '$name' created." + } + elseif ($existingRules) { + # Either name or group was specified which matched existing rules, check the properties + $changedRules = [System.Collections.Generic.List[String]]@() + $unchangedRules = [System.Collections.Generic.List[String]]@() + + foreach ($existingRule in $existingRules) { + if ($diff_support) { + $result.diff.prepared += "[$($existingRule.Name)]`n" + } + + $changed = $false + for ($i = 0; $i -lt $fwPropertiesToCompare.Length; $i++) { + $prop = $fwPropertiesToCompare[$i] + if ($null -ne $userPassedArguments[$i]) { + # only change values the user passes in task definition + if ($existingRule.$prop -ne $new_rule.$prop) { + $haveSameAddresses = $false + if ($prop -like "*Addresses") { + $existingAddresses = $existingRule.$prop -split ',' + $newAddresses = $new_rule.$prop -split ',' + if (-not (Compare-Object $existingAddresses $newAddresses)) { + $haveSameAddresses = $true + } + } + if (-not $haveSameAddresses) { + if ($diff_support) { + $result.diff.prepared += "-$($prop)='$($existingRule.$prop)'`n" + $result.diff.prepared += "+$($prop)='$($new_rule.$prop)'`n" + } + + if (-not $check_mode) { + # Profiles value cannot be a uint32, but the "all profiles" value (0x7FFFFFFF) will often become a uint32, + # so must cast to [int] to prevent InvalidCastException under PS5+ + If ($prop -eq 'Profiles') { + $existingRule.Profiles = [int] $new_rule.$prop + } + # There is a fundamental problem with the COM binder in PowerShell and how it treats null values. + ElseIf ($prop -eq 'ApplicationName') { + [Community.Windows.WinFirewallRule.NetFwRule]::PutApplicationName($existingRule, $new_rule.$prop) + } + Else { + $existingRule.$prop = $new_rule.$prop + } + } + $changed = $true + } + } + } + } + + if ($changed) { + $result.changed = $true + $changedRules.Add($existingRule.Name) + } + else { + $unchangedRules.Add($existingRule.Name) + } + } + + $result.msg = "Firewall rule(s) changed '$($changedRules -join "', '")' - unchanged '$($unchangedRules -join "', '")'" + } + } +} +catch [Exception] { + $ex = $_ + $result['exception'] = $($ex | Out-String) + Fail-Json $result $ex.Exception.Message +} + +Exit-Json $result diff --git a/ansible_collections/community/windows/plugins/modules/win_firewall_rule.py b/ansible_collections/community/windows/plugins/modules/win_firewall_rule.py new file mode 100644 index 000000000..111d45a1c --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_firewall_rule.py @@ -0,0 +1,196 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2014, Timothy Vandenbrande <timothy.vandenbrande@gmail.com> +# Copyright: (c) 2017, Artem Zinenko <zinenkoartem@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_firewall_rule +short_description: Windows firewall automation +description: + - Allows you to create/remove/update firewall rules. +options: + enabled: + description: + - Whether this firewall rule is enabled or disabled. + - Defaults to C(true) when creating a new rule. + type: bool + aliases: [ enable ] + state: + description: + - Should this rule be added or removed. + type: str + choices: [ absent, present ] + default: present + name: + description: + - The rule's display name. + - This is required unless I(group) is specified. + type: str + group: + description: + - The group name for the rule. + - If I(name) is not specified then the module will set the firewall options for all the rules in this group. + type: str + direction: + description: + - Whether this rule is for inbound or outbound traffic. + - Defaults to C(in) when creating a new rule. + type: str + choices: [ in, out ] + action: + description: + - What to do with the items this rule is for. + - Defaults to C(allow) when creating a new rule. + type: str + choices: [ allow, block ] + description: + description: + - Description for the firewall rule. + type: str + localip: + description: + - The local ip address this rule applies to. + - Set to C(any) to apply to all local ip addresses. + - Defaults to C(any) when creating a new rule. + type: str + remoteip: + description: + - The remote ip address/range this rule applies to. + - Set to C(any) to apply to all remote ip addresses. + - Defaults to C(any) when creating a new rule. + type: str + localport: + description: + - The local port this rule applies to. + - Set to C(any) to apply to all local ports. + - Defaults to C(any) when creating a new rule. + - Must have I(protocol) set + type: str + remoteport: + description: + - The remote port this rule applies to. + - Set to C(any) to apply to all remote ports. + - Defaults to C(any) when creating a new rule. + - Must have I(protocol) set + type: str + program: + description: + - The program this rule applies to. + - Set to C(any) to apply to all programs. + - Defaults to C(any) when creating a new rule. + type: str + service: + description: + - The service this rule applies to. + - Set to C(any) to apply to all services. + - Defaults to C(any) when creating a new rule. + type: str + protocol: + description: + - The protocol this rule applies to. + - Set to C(any) to apply to all services. + - Defaults to C(any) when creating a new rule. + type: str + profiles: + description: + - The profile this rule applies to. + - Defaults to C(domain,private,public) when creating a new rule. + type: list + elements: str + aliases: [ profile ] + icmp_type_code: + description: + - The ICMP types and codes for the rule. + - This is only valid when I(protocol) is C(icmpv4) or C(icmpv6). + - Each entry follows the format C(type:code) where C(type) is the type + number and C(code) is the code number for that type or C(*) for all + codes. + - Set the value to just C(*) to apply the rule for all ICMP type codes. + - See U(https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml) + for a list of ICMP types and the codes that apply to them. + type: list + elements: str +notes: +- Multiple firewall rules can share the same I(name), if there are multiple matches then the module will set the user + defined options for each matching rule. +seealso: +- module: community.windows.win_firewall +author: + - Artem Zinenko (@ar7z1) + - Timothy Vandenbrande (@TimothyVandenbrande) +''' + +EXAMPLES = r''' +- name: Firewall rule to allow SMTP on TCP port 25 + community.windows.win_firewall_rule: + name: SMTP + localport: 25 + action: allow + direction: in + protocol: tcp + state: present + enabled: yes + +- name: Firewall rule to allow RDP on TCP port 3389 + community.windows.win_firewall_rule: + name: Remote Desktop + localport: 3389 + action: allow + direction: in + protocol: tcp + profiles: private + state: present + enabled: yes + +- name: Firewall rule to be created for application group + community.windows.win_firewall_rule: + name: SMTP + group: application + localport: 25 + action: allow + direction: in + protocol: tcp + state: present + enabled: yes + +- name: Enable all the Firewall rules in application group + win_firewall_rule: + group: application + enabled: yes + +- name: Firewall rule to allow port range + community.windows.win_firewall_rule: + name: Sample port range + localport: 5000-5010 + action: allow + direction: in + protocol: tcp + state: present + enabled: yes + +- name: Firewall rule to allow ICMP v4 echo (ping) + community.windows.win_firewall_rule: + name: ICMP Allow incoming V4 echo request + enabled: yes + state: present + profiles: private + action: allow + direction: in + protocol: icmpv4 + icmp_type_code: + - '8:*' + +- name: Firewall rule to alloc ICMP v4 on all type codes + community.windows.win_firewall_rule: + name: ICMP Allow incoming V4 echo request + enabled: yes + state: present + profiles: private + action: allow + direction: in + protocol: icmpv4 + icmp_type_code: '*' +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_format.ps1 b/ansible_collections/community/windows/plugins/modules/win_format.ps1 new file mode 100644 index 000000000..0746fbe47 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_format.ps1 @@ -0,0 +1,232 @@ +#!powershell + +# Copyright: (c) 2019, Varun Chopra (@chopraaa) <v@chopraaa.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic +#AnsibleRequires -OSVersion 6.2 + +Set-StrictMode -Version 2 + +$ErrorActionPreference = "Stop" + +$spec = @{ + options = @{ + drive_letter = @{ type = "str" } + path = @{ type = "str" } + label = @{ type = "str" } + new_label = @{ type = "str" } + file_system = @{ type = "str"; choices = "ntfs", "refs", "exfat", "fat32", "fat" } + allocation_unit_size = @{ type = "int" } + large_frs = @{ type = "bool" } + full = @{ type = "bool"; default = $false } + compress = @{ type = "bool" } + integrity_streams = @{ type = "bool" } + force = @{ type = "bool"; default = $false } + } + mutually_exclusive = @( + , @('drive_letter', 'path', 'label') + ) + required_one_of = @( + , @('drive_letter', 'path', 'label') + ) + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$drive_letter = $module.Params.drive_letter +$path = $module.Params.path +$label = $module.Params.label +$new_label = $module.Params.new_label +$file_system = $module.Params.file_system +$allocation_unit_size = $module.Params.allocation_unit_size +$large_frs = $module.Params.large_frs +$full_format = $module.Params.full +$compress_volume = $module.Params.compress +$integrity_streams = $module.Params.integrity_streams +$force_format = $module.Params.force + +# Some pre-checks +if ($null -ne $drive_letter -and $drive_letter -notmatch "^[a-zA-Z]$") { + $module.FailJson("The parameter drive_letter should be a single character A-Z") +} +if ($integrity_streams -eq $true -and $file_system -ne "refs") { + $module.FailJson("Integrity streams can be enabled only on ReFS volumes. You specified: $($file_system)") +} +if ($compress_volume -eq $true) { + if ($file_system -eq "ntfs") { + if ($null -ne $allocation_unit_size -and $allocation_unit_size -gt 4096) { + $module.FailJson("NTFS compression is not supported for allocation unit sizes above 4096") + } + } + else { + $module.FailJson("Compression can be enabled only on NTFS volumes. You specified: $($file_system)") + } +} + +function Get-AnsibleVolume { + param( + $DriveLetter, + $Path, + $Label + ) + + if ($null -ne $DriveLetter) { + try { + $volume = Get-Volume -DriveLetter $DriveLetter + } + catch { + $module.FailJson("There was an error retrieving the volume using drive_letter $($DriveLetter): $($_.Exception.Message)", $_) + } + } + elseif ($null -ne $Path) { + try { + $volume = Get-Volume -Path $Path + } + catch { + $module.FailJson("There was an error retrieving the volume using path $($Path): $($_.Exception.Message)", $_) + } + } + elseif ($null -ne $Label) { + try { + $volume = Get-Volume -FileSystemLabel $Label + } + catch { + $module.FailJson("There was an error retrieving the volume using label $($Label): $($_.Exception.Message)", $_) + } + } + else { + $module.FailJson("Unable to locate volume: drive_letter, path and label were not specified") + } + + return $volume +} + +function Format-AnsibleVolume { + param( + $Path, + $Label, + $FileSystem, + $Full, + $UseLargeFRS, + $Compress, + $SetIntegrityStreams, + $AllocationUnitSize + ) + $parameters = @{ + Path = $Path + Full = $Full + } + if ($null -ne $UseLargeFRS) { + $parameters.Add("UseLargeFRS", $UseLargeFRS) + } + if ($null -ne $SetIntegrityStreams) { + $parameters.Add("SetIntegrityStreams", $SetIntegrityStreams) + } + if ($null -ne $Compress) { + $parameters.Add("Compress", $Compress) + } + if ($null -ne $Label) { + $parameters.Add("NewFileSystemLabel", $Label) + } + if ($null -ne $FileSystem) { + $parameters.Add("FileSystem", $FileSystem) + } + if ($null -ne $AllocationUnitSize) { + $parameters.Add("AllocationUnitSize", $AllocationUnitSize) + } + + Format-Volume @parameters -Confirm:$false | Out-Null + +} + +$ansible_volume = Get-AnsibleVolume -DriveLetter $drive_letter -Path $path -Label $label +$ansible_file_system = $ansible_volume.FileSystem +$ansible_volume_size = $ansible_volume.Size +$ansible_volume_alu = (Get-CimInstance -ClassName Win32_Volume -Filter "DeviceId = '$($ansible_volume.path.replace('\','\\'))'" -Property BlockSize).BlockSize + +$ansible_partition = Get-Partition -Volume $ansible_volume + +if ( + -not $force_format -and + $null -ne $allocation_unit_size -and + $ansible_volume_alu -ne 0 -and + $null -ne $ansible_volume_alu -and + $allocation_unit_size -ne $ansible_volume_alu +) { + $msg = -join @( + "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)" + ) + $module.FailJson($msg) +} + +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) { + $msg = -join @( + "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())" + ) + $module.FailJson($msg) + } + 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_params = @{ + 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 + } + Format-AnsibleVolume @format_params + } + $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_params = @{ + 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 + } + Format-AnsibleVolume @format_params + } + $module.Result.changed = $true + } + } +} + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_format.py b/ansible_collections/community/windows/plugins/modules/win_format.py new file mode 100644 index 000000000..3ff93de0b --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_format.py @@ -0,0 +1,98 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Varun Chopra (@chopraaa) <v@chopraaa.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +module: win_format +short_description: Formats an existing volume or a new volume on an existing partition on Windows +description: + - The M(community.windows.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 + default: no + 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 + default: no +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: community.windows.win_disk_facts + - module: community.windows.win_partition +author: + - Varun Chopra (@chopraaa) <v@chopraaa.com> +''' + +EXAMPLES = r''' +- name: Create a partition with drive letter D and size 5 GiB + community.windows.win_partition: + drive_letter: D + partition_size: 5 GiB + disk_number: 1 + +- name: Full format the newly created partition as NTFS and label it + community.windows.win_format: + drive_letter: D + file_system: NTFS + new_label: Formatted + full: True +''' + +RETURN = r''' +# +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_hosts.ps1 b/ansible_collections/community/windows/plugins/modules/win_hosts.ps1 new file mode 100644 index 000000000..89766d37d --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_hosts.ps1 @@ -0,0 +1,268 @@ +#!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-HostEntryPart($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-HostEntryPart -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-HostEntryPart -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-HostEntryPart -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/ansible_collections/community/windows/plugins/modules/win_hosts.py b/ansible_collections/community/windows/plugins/modules/win_hosts.py new file mode 100644 index 000000000..ba583a567 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_hosts.py @@ -0,0 +1,119 @@ +#!/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) + +DOCUMENTATION = r''' +--- +module: win_hosts +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 + elements: str + 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: ansible.windows.win_template + - module: ansible.windows.win_file + - module: ansible.windows.win_copy +''' + +EXAMPLES = r''' +- name: Add 127.0.0.1 as an A record for localhost + community.windows.win_hosts: + state: present + canonical_name: localhost + ip_address: 127.0.0.1 + +- name: Add ::1 as an AAAA record for localhost + community.windows.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) + community.windows.win_hosts: + state: present + canonical_name: foo + ip_address: 192.168.1.100 + action: remove + aliases: + - bar + - zed + +- name: Remove hosts entries with canonical name 'bar' + community.windows.win_hosts: + state: absent + canonical_name: bar + +- name: Remove 10.2.0.1 from the list of hosts + community.windows.win_hosts: + state: absent + ip_address: 10.2.0.1 + +- name: Ensure all name resolution is handled by DNS + community.windows.win_hosts: + state: absent +''' + +RETURN = r''' +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_hotfix.ps1 b/ansible_collections/community/windows/plugins/modules/win_hotfix.ps1 new file mode 100644 index 000000000..2ef663d2f --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_hotfix.ps1 @@ -0,0 +1,270 @@ +#!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" -type "bool" -default $false + +$hotfix_kb = Get-AnsibleParam -obj $params -name "hotfix_kb" -type "str" +$hotfix_identifier = Get-AnsibleParam -obj $params -name "hotfix_identifier" -type "str" +$state = Get-AnsibleParam -obj $params -name "state" -type "state" -default "present" -validateset "absent", "present" +$source = Get-AnsibleParam -obj $params -name "source" -type "path" + +$result = @{ + changed = $false + reboot_required = $false +} + +if (Get-Module -Name DISM -ListAvailable) { + Import-Module -Name DISM +} +else { + # Server 2008 R2 doesn't have the DISM module installed on the path, check the Windows ADK path + $adk_root = [System.Environment]::ExpandEnvironmentVariables("%PROGRAMFILES(X86)%\Windows Kits\*\Assessment and Deployment Kit\Deployment Tools\amd64\DISM") + if (Test-Path -LiteralPath $adk_root) { + Import-Module -Name (Get-Item -LiteralPath $adk_root).FullName + } + else { + Fail-Json $result "The DISM PS module needs to be installed, this can be done through the windows-adk chocolately package" + } +} + + +Function Expand-MSU($msu) { + $temp_path = [IO.Path]::GetTempPath() + $temp_foldername = [Guid]::NewGuid() + $output_path = Join-Path -Path $temp_path -ChildPath $temp_foldername + New-Item -Path $output_path -ItemType Directory | Out-Null + + $expand_args = @($msu, $output_path, "-F:*") + + try { + &expand.exe $expand_args | Out-NUll + } + catch { + Fail-Json $result "failed to run expand.exe $($expand_args): $($_.Exception.Message)" + } + if ($LASTEXITCODE -ne 0) { + Fail-Json $result "failed to run expand.exe $($expand_args): RC = $LASTEXITCODE" + } + + return $output_path +} + +Function Get-HotfixMetadataFromName($name) { + try { + $dism_package_info = Get-WindowsPackage -Online -PackageName $name + } + catch { + # build a basic stub for a missing result + $dism_package_info = @{ + PackageState = "NotPresent" + Description = "" + PackageName = $name + } + } + + if ($dism_package_info.Description -match "(KB\d*)") { + $hotfix_kb = $Matches[0] + } + else { + $hotfix_kb = "UNKNOWN" + } + + $metadata = @{ + name = $dism_package_info.PackageName + state = $dism_package_info.PackageState + kb = $hotfix_kb + } + + return $metadata +} + +Function Get-HotfixMetadataFromFile($extract_path) { + # MSU contents https://support.microsoft.com/en-us/help/934307/description-of-the-windows-update-standalone-installer-in-windows + $metadata_path = Get-ChildItem -LiteralPath $extract_path | Where-Object { $_.Extension -eq ".xml" } + if ($null -eq $metadata_path) { + Fail-Json $result "failed to get metadata xml inside MSU file, cannot get hotfix metadata required for this task" + } + [xml]$xml = Get-Content -LiteralPath $metadata_path.FullName + + $xml.unattend.servicing.package.source.location | ForEach-Object { + $cab_source_filename = Split-Path -Path $_ -Leaf + $cab_file = Join-Path -Path $extract_path -ChildPath $cab_source_filename + + try { + $dism_package_info = Get-WindowsPackage -Online -PackagePath $cab_file + } + catch { + Fail-Json $result "failed to get DISM package metadata from path $($extract_path): $($_.Exception.Message)" + } + if ($dism_package_info.Applicable -eq $false) { + Fail-Json $result "hotfix package is not applicable for this server" + } + + $package_properties_path = Get-ChildItem -LiteralPath $extract_path | Where-Object { $_.Extension -eq ".txt" } + if ($null -eq $package_properties_path) { + $hotfix_kb = "UNKNOWN" + } + else { + $package_ini = Get-Content -LiteralPath $package_properties_path.FullName + $entry = $package_ini | Where-Object { $_.StartsWith("KB Article Number") } + if ($null -eq $entry) { + $hotfix_kb = "UNKNOWN" + } + else { + $hotfix_kb = ($entry -split '=')[-1] + $hotfix_kb = "KB$($hotfix_kb.Substring(1, $hotfix_kb.Length - 2))" + } + } + + [pscustomobject]@{ + path = $cab_file + name = $dism_package_info.PackageName + state = $dism_package_info.PackageState + kb = $hotfix_kb + } + } +} + +Function Get-HotfixMetadataFromKB($kb) { + # I really hate doing it this way + $packages = Get-WindowsPackage -Online + $identifier = $packages | Where-Object { $_.PackageName -like "*$kb*" } + + if ($null -eq $identifier) { + # still haven't found the KB, need to loop through the results and check the description + foreach ($package in $packages) { + $raw_metadata = Get-HotfixMetadataFromName -name $package.PackageName + if ($raw_metadata.kb -eq $kb) { + $identifier = $raw_metadata + break + } + } + + # if we still haven't found the package then we need to throw an error + if ($null -eq $metadata) { + Fail-Json $result "failed to get DISM package from KB, to continue specify hotfix_identifier instead" + } + } + else { + $metadata = Get-HotfixMetadataFromName -name $identifier.PackageName + } + + return $metadata +} + +if ($state -eq "absent") { + # uninstall hotfix + # this is a pretty poor way of doing this, is there a better way? + + if ($null -ne $hotfix_identifier) { + $hotfix_metadata = Get-HotfixMetadataFromName -name $hotfix_identifier + } + elseif ($null -ne $hotfix_kb) { + $hotfix_install_info = Get-Hotfix -Id $hotfix_kb -ErrorAction SilentlyContinue + if ($null -ne $hotfix_install_info) { + $hotfix_metadata = Get-HotfixMetadataFromKB -kb $hotfix_kb + } + else { + $hotfix_metadata = @{state = "NotPresent" } + } + } + else { + Fail-Json $result "either hotfix_identifier or hotfix_kb needs to be set when state=absent" + } + + # how do we want to deal with the other states? + if ($hotfix_metadata.state -eq "UninstallPending") { + $result.identifier = $hotfix_metadata.name + $result.kb = $hotfix_metadata.kb + $result.reboot_required = $true + } + elseif ($hotfix_metadata.state -eq "Installed") { + $result.identifier = $hotfix_metadata.name + $result.kb = $hotfix_metadata.kb + + if (-not $check_mode) { + try { + $remove_result = Remove-WindowsPackage -Online -PackageName $hotfix_metadata.name -NoRestart + } + catch { + Fail-Json $result "failed to remove package $($hotfix_metadata.name): $($_.Exception.Message)" + } + $result.reboot_required = $remove_Result.RestartNeeded + } + + $result.changed = $true + } +} +else { + if ($null -eq $source) { + Fail-Json $result "source must be set when state=present" + } + if (-not (Test-Path -LiteralPath $source -PathType Leaf)) { + Fail-Json $result "the path set for source $source does not exist or is not a file" + } + + # while we do extract the file in check mode we need to do so for valid checking + $extract_path = Expand-MSU -msu $source + try { + $hotfix_metadata = Get-HotfixMetadataFromFile -extract_path $extract_path + + # validate the hotfix matches if the hotfix id has been passed in + if ($null -ne $hotfix_identifier) { + if ($hotfix_metadata.name -ne $hotfix_identifier) { + $msg = -join @( + "the hotfix identifier $hotfix_identifier does not match with the source msu identifier $($hotfix_metadata.name), " + "please omit or specify the correct identifier to continue" + ) + Fail-Json $result $msg + } + } + if ($null -ne $hotfix_kb) { + if ($hotfix_metadata.kb -ne $hotfix_kb) { + $msg = -join @( + "the hotfix KB $hotfix_kb does not match with the source msu KB $($hotfix_metadata.kb), " + "please omit or specify the correct KB to continue" + ) + Fail-Json $result $msg + } + } + + $result.identifiers = @($hotfix_metadata.name) + $result.identifier = $result.identifiers[0] + $result.kbs = @($hotfix_metadata.kb) + $result.kb = $result.kbs[0] + + # how do we want to deal with other states + if ($hotfix_metadata.state -eq "InstallPending") { + # return the reboot required flag, should we fail here instead + $result.reboot_required = $true + } + elseif ($hotfix_metadata.state -ne "Installed") { + if (-not $check_mode) { + try { + $install_result = @( + Foreach ($path in $hotfix_metadata.path) { + Add-WindowsPackage -Online -PackagePath $path -NoRestart + } + ) + } + catch { + Fail-Json $result "failed to add windows package from path $($hotfix_metadata.path): $($_.Exception.Message)" + } + $result.reboot_required = [bool]($install_result.RestartNeeded -eq $true) + } + $result.changed = $true + } + } + finally { + Remove-Item -LiteralPath $extract_path -Force -Recurse + } +} + +Exit-Json $result diff --git a/ansible_collections/community/windows/plugins/modules/win_hotfix.py b/ansible_collections/community/windows/plugins/modules/win_hotfix.py new file mode 100644 index 000000000..41d03fb96 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_hotfix.py @@ -0,0 +1,150 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_hotfix +short_description: Install and uninstalls Windows hotfixes +description: +- Install, uninstall a Windows hotfix. +options: + hotfix_identifier: + description: + - The name of the hotfix as shown in DISM, see examples for details. + - This or C(hotfix_kb) MUST be set when C(state=absent). + - If C(state=present) then the hotfix at C(source) will be validated + against this value, if it does not match an error will occur. + - You can get the identifier by running + 'Get-WindowsPackage -Online -PackagePath path-to-cab-in-msu' after + expanding the msu file. + type: str + hotfix_kb: + description: + - The name of the KB the hotfix relates to, see examples for details. + - This or C(hotfix_identifier) MUST be set when C(state=absent). + - If C(state=present) then the hotfix at C(source) will be validated + against this value, if it does not match an error will occur. + - Because DISM uses the identifier as a key and doesn't refer to a KB in + all cases it is recommended to use C(hotfix_identifier) instead. + type: str + state: + description: + - Whether to install or uninstall the hotfix. + - When C(present), C(source) MUST be set. + - When C(absent), C(hotfix_identifier) or C(hotfix_kb) MUST be set. + type: str + default: present + choices: [ absent, present ] + source: + description: + - The path to the downloaded hotfix .msu file. + - This MUST be set if C(state=present) and MUST be a .msu hotfix file. + type: path +notes: +- This must be run on a host that has the DISM powershell module installed and + a Powershell version >= 4. +- This module is installed by default on Windows 8 and Server 2012 and newer. +- You can manually install this module on Windows 7 and Server 2008 R2 by + installing the Windows ADK + U(https://developer.microsoft.com/en-us/windows/hardware/windows-assessment-deployment-kit), + see examples to see how to do it with chocolatey. +- You can download hotfixes from U(https://www.catalog.update.microsoft.com/Home.aspx). +seealso: +- module: ansible.windows.win_package +- module: ansible.windows.win_updates +author: +- Jordan Borean (@jborean93) +''' + +EXAMPLES = r''' +- name: Install Windows ADK with DISM for Server 2008 R2 + chocolatey.chocolatey.win_chocolatey: + name: windows-adk + version: 8.100.26866.0 + state: present + install_args: /features OptionId.DeploymentTools + +- name: Install hotfix without validating the KB and Identifier + community.windows.win_hotfix: + source: C:\temp\windows8.1-kb3172729-x64_e8003822a7ef4705cbb65623b72fd3cec73fe222.msu + state: present + register: hotfix_install + +- ansible.windows.win_reboot: + when: hotfix_install.reboot_required + +- name: Install hotfix validating KB + community.windows.win_hotfix: + hotfix_kb: KB3172729 + source: C:\temp\windows8.1-kb3172729-x64_e8003822a7ef4705cbb65623b72fd3cec73fe222.msu + state: present + register: hotfix_install + +- ansible.windows.win_reboot: + when: hotfix_install.reboot_required + +- name: Install hotfix validating Identifier + community.windows.win_hotfix: + hotfix_identifier: Package_for_KB3172729~31bf3856ad364e35~amd64~~6.3.1.0 + source: C:\temp\windows8.1-kb3172729-x64_e8003822a7ef4705cbb65623b72fd3cec73fe222.msu + state: present + register: hotfix_install + +- ansible.windows.win_reboot: + when: hotfix_install.reboot_required + +- name: Uninstall hotfix with Identifier + community.windows.win_hotfix: + hotfix_identifier: Package_for_KB3172729~31bf3856ad364e35~amd64~~6.3.1.0 + state: absent + register: hotfix_uninstall + +- ansible.windows.win_reboot: + when: hotfix_uninstall.reboot_required + +- name: Uninstall hotfix with KB (not recommended) + community.windows.win_hotfix: + hotfix_kb: KB3172729 + state: absent + register: hotfix_uninstall + +- ansible.windows.win_reboot: + when: hotfix_uninstall.reboot_required +''' + +RETURN = r''' +identifier: + description: The DISM identifier for the hotfix. + returned: success + type: str + sample: Package_for_KB3172729~31bf3856ad364e35~amd64~~6.3.1.0 +identifiers: + description: The DISM identifiers for each hotfix in the msu. + returned: success + type: list + elements: str + sample: + - Package_for_KB3172729~31bf3856ad364e35~amd64~~6.3.1.0 + version_added: '1.10.0' +kb: + description: The KB the hotfix relates to. + returned: success + type: str + sample: KB3172729 +kbs: + description: The KB for each hotfix in the msu, + returned: success + type: list + elements: str + sample: + - KB3172729 + version_added: '1.10.0' +reboot_required: + description: Whether a reboot is required for the install or uninstall to + finalise. + returned: success + type: str + sample: true +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_http_proxy.ps1 b/ansible_collections/community/windows/plugins/modules/win_http_proxy.ps1 new file mode 100644 index 000000000..3b564cae2 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_http_proxy.ps1 @@ -0,0 +1,268 @@ +#!powershell + +# Copyright: (c) 2019, 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.AddType + +$spec = @{ + options = @{ + bypass = @{ type = "list"; elements = "str"; no_log = $false } + proxy = @{ type = "raw" } + source = @{ type = "str"; choices = @("ie") } + } + mutually_exclusive = @( + @("proxy", "source"), + @("bypass", "source") + ) + required_by = @{ + bypass = @("proxy") + } + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$proxy = $module.Params.proxy +$bypass = $module.Params.bypass +$source = $module.Params.source + +# Parse the raw value, it should be a Dictionary or String +if ($proxy -is [System.Collections.IDictionary]) { + $valid_keys = [System.Collections.Generic.List`1[String]]@("http", "https", "ftp", "socks") + # Check to make sure we don't have any invalid keys in the dict + $invalid_keys = [System.Collections.Generic.List`1[String]]@() + foreach ($k in $proxy.Keys) { + if ($k -notin $valid_keys) { + $invalid_keys.Add($k) + } + } + + if ($invalid_keys.Count -gt 0) { + $invalid_keys = $invalid_keys | Sort-Object # So our test assertion doesn't fail due to random ordering + $module.FailJson("Invalid keys found in proxy: $($invalid_keys -join ', '). Valid keys are $($valid_keys -join ', ').") + } + + # Build the proxy string in the form 'protocol=host;', the order of valid_keys is also important + $proxy_list = [System.Collections.Generic.List`1[String]]@() + foreach ($k in $valid_keys) { + if ($proxy.ContainsKey($k)) { + $proxy_list.Add("$k=$($proxy.$k)") + } + } + $proxy = $proxy_list -join ";" +} +elseif ($null -ne $proxy) { + $proxy = $proxy.ToString() +} + +if ($bypass) { + if ([System.String]::IsNullOrEmpty($proxy)) { + $module.FailJson("missing parameter(s) required by ''bypass'': proxy") + } + $bypass = $bypass -join ';' +} + +$win_http_invoke = @' +using System; +using System.Runtime.InteropServices; + +namespace Ansible.WinHttpProxy +{ + internal class NativeHelpers + { + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public class WINHTTP_CURRENT_USER_IE_PROXY_CONFIG : IDisposable + { + public bool fAutoDetect; + public IntPtr lpszAutoConfigUrl; + public IntPtr lpszProxy; + public IntPtr lpszProxyBypass; + + public void Dispose() + { + if (lpszAutoConfigUrl != IntPtr.Zero) + Marshal.FreeHGlobal(lpszAutoConfigUrl); + if (lpszProxy != IntPtr.Zero) + Marshal.FreeHGlobal(lpszProxy); + if (lpszProxyBypass != IntPtr.Zero) + Marshal.FreeHGlobal(lpszProxyBypass); + GC.SuppressFinalize(this); + } + ~WINHTTP_CURRENT_USER_IE_PROXY_CONFIG() { this.Dispose(); } + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public class WINHTTP_PROXY_INFO : IDisposable + { + public UInt32 dwAccessType; + public IntPtr lpszProxy; + public IntPtr lpszProxyBypass; + + public void Dispose() + { + if (lpszProxy != IntPtr.Zero) + Marshal.FreeHGlobal(lpszProxy); + if (lpszProxyBypass != IntPtr.Zero) + Marshal.FreeHGlobal(lpszProxyBypass); + GC.SuppressFinalize(this); + } + ~WINHTTP_PROXY_INFO() { this.Dispose(); } + } + } + + internal class NativeMethods + { + [DllImport("Winhttp.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool WinHttpGetDefaultProxyConfiguration( + [Out] NativeHelpers.WINHTTP_PROXY_INFO pProxyInfo); + + [DllImport("Winhttp.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool WinHttpGetIEProxyConfigForCurrentUser( + [Out] NativeHelpers.WINHTTP_CURRENT_USER_IE_PROXY_CONFIG pProxyConfig); + + [DllImport("Winhttp.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool WinHttpSetDefaultProxyConfiguration( + NativeHelpers.WINHTTP_PROXY_INFO pProxyInfo); + } + + 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 WinINetProxy + { + public bool AutoDetect; + public string AutoConfigUrl; + public string Proxy; + public string ProxyBypass; + } + + public class WinHttpProxy + { + public string Proxy; + public string ProxyBypass; + + public WinHttpProxy() + { + Refresh(); + } + + public void Set() + { + using (NativeHelpers.WINHTTP_PROXY_INFO proxyInfo = new NativeHelpers.WINHTTP_PROXY_INFO()) + { + if (String.IsNullOrEmpty(Proxy)) + proxyInfo.dwAccessType = 1; // WINHTTP_ACCESS_TYPE_NO_PROXY + else + { + proxyInfo.dwAccessType = 3; // WINHTTP_ACCESS_TYPE_NAMED_PROXY + proxyInfo.lpszProxy = Marshal.StringToHGlobalUni(Proxy); + + if (!String.IsNullOrEmpty(ProxyBypass)) + proxyInfo.lpszProxyBypass = Marshal.StringToHGlobalUni(ProxyBypass); + } + + if (!NativeMethods.WinHttpSetDefaultProxyConfiguration(proxyInfo)) + throw new Win32Exception("WinHttpSetDefaultProxyConfiguration() failed"); + } + } + + public void Refresh() + { + using (NativeHelpers.WINHTTP_PROXY_INFO proxyInfo = new NativeHelpers.WINHTTP_PROXY_INFO()) + { + if (!NativeMethods.WinHttpGetDefaultProxyConfiguration(proxyInfo)) + throw new Win32Exception("WinHttpGetDefaultProxyConfiguration() failed"); + + Proxy = Marshal.PtrToStringUni(proxyInfo.lpszProxy); + ProxyBypass = Marshal.PtrToStringUni(proxyInfo.lpszProxyBypass); + } + } + + public static WinINetProxy GetIEProxyConfig() + { + using (NativeHelpers.WINHTTP_CURRENT_USER_IE_PROXY_CONFIG ieProxy = new NativeHelpers.WINHTTP_CURRENT_USER_IE_PROXY_CONFIG()) + { + if (!NativeMethods.WinHttpGetIEProxyConfigForCurrentUser(ieProxy)) + throw new Win32Exception("WinHttpGetIEProxyConfigForCurrentUser() failed"); + + return new WinINetProxy + { + AutoDetect = ieProxy.fAutoDetect, + AutoConfigUrl = Marshal.PtrToStringUni(ieProxy.lpszAutoConfigUrl), + Proxy = Marshal.PtrToStringUni(ieProxy.lpszProxy), + ProxyBypass = Marshal.PtrToStringUni(ieProxy.lpszProxyBypass), + }; + } + } + } +} +'@ +Add-CSharpType -References $win_http_invoke -AnsibleModule $module + +$actual_proxy = New-Object -TypeName Ansible.WinHttpProxy.WinHttpProxy + +$module.Diff.before = @{ + proxy = $actual_proxy.Proxy + bypass = $actual_proxy.ProxyBypass +} + +if ($source -eq "ie") { + # If source=ie we need to get the server and bypass values from the IE configuration + $ie_proxy = [Ansible.WinHttpProxy.WinHttpProxy]::GetIEProxyConfig() + $proxy = $ie_proxy.Proxy + $bypass = $ie_proxy.ProxyBypass +} + +$previous_proxy = $actual_proxy.Proxy +$previous_bypass = $actual_proxy.ProxyBypass + +# Make sure an empty string is converted to $null for easier comparisons +if ([String]::IsNullOrEmpty($proxy)) { + $proxy = $null +} +if ([String]::IsNullOrEmpty($bypass)) { + $bypass = $null +} + +if ($previous_proxy -ne $proxy -or $previous_bypass -ne $bypass) { + $actual_proxy.Proxy = $proxy + $actual_proxy.ProxyBypass = $bypass + + if (-not $module.CheckMode) { + $actual_proxy.Set() + + # Validate that the change was made correctly and revert if it wasn't. The Set() method won't fail on invalid + # values so we need to check again to make sure all was good. + $actual_proxy.Refresh() + if ($actual_proxy.Proxy -ne $proxy -or $actual_proxy.ProxyBypass -ne $bypass) { + $actual_proxy.Proxy = $previous_proxy + $actual_proxy.ProxyBypass = $previous_bypass + $actual_proxy.Set() + + $module.FailJson("Unknown error when trying to set proxy '$proxy' or bypass '$bypass'") + } + } + + $module.Result.changed = $true +} + +$module.Diff.after = @{ + proxy = $proxy + bypass = $bypass +} + +$module.ExitJson() + diff --git a/ansible_collections/community/windows/plugins/modules/win_http_proxy.py b/ansible_collections/community/windows/plugins/modules/win_http_proxy.py new file mode 100644 index 000000000..92f7e5c68 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_http_proxy.py @@ -0,0 +1,100 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_http_proxy +short_description: Manages proxy settings for WinHTTP +description: +- Used to set, remove, or import proxy settings for Windows HTTP Services + C(WinHTTP). +- WinHTTP is a framework used by applications or services, typically .NET + applications or non-interactive services, to make web requests. +options: + bypass: + description: + - A list of hosts that will bypass the set proxy when being accessed. + - Use C(<local>) to match hostnames that are not fully qualified domain + names. This is useful when needing to connect to intranet sites using + just the hostname. + - Omit, set to null or an empty string/list to remove the bypass list. + - If this is set then I(proxy) must also be set. + type: list + elements: str + proxy: + description: + - A string or dict that specifies the proxy to be set. + - If setting a string, should be in the form C(hostname), C(hostname:port), + or C(protocol=hostname:port). + - If the port is undefined, the default port for the protocol in use is + used. + - If setting a dict, the keys should be the protocol and the values should + be the hostname and/or port for that protocol. + - Valid protocols are C(http), C(https), C(ftp), and C(socks). + - Omit, set to null or an empty string to remove the proxy settings. + type: raw + source: + description: + - Instead of manually specifying the I(proxy) and/or I(bypass), set this to + import the proxy from a set source like Internet Explorer. + - Using C(ie) will import the Internet Explorer proxy settings for the + current active network connection of the current user. + - Only IE's proxy URL and bypass list will be imported into WinHTTP. + - This is like running C(netsh winhttp import proxy source=ie). + - The value is imported when the module runs and will not automatically + be updated if the IE configuration changes in the future. The module will + have to be run again to sync the latest changes. + choices: + - ie + type: str +notes: +- This is not the same as the proxy settings set in Internet Explorer, also + known as C(WinINet); use the M(community.windows.win_inet_proxy) module to manage that instead. +- These settings are set system wide and not per user, it will require + Administrative privileges to run. +seealso: +- module: community.windows.win_inet_proxy +author: +- Jordan Borean (@jborean93) +''' + +EXAMPLES = r''' +- name: Set a proxy to use for all protocols + community.windows.win_http_proxy: + proxy: hostname + +- name: Set a proxy with a specific port with a bypass list + community.windows.win_http_proxy: + proxy: hostname:8080 + bypass: + - server1 + - server2 + - <local> + +- name: Set the proxy based on the IE proxy settings + community.windows.win_http_proxy: + source: ie + +- name: Set a proxy for specific protocols + community.windows.win_http_proxy: + proxy: + http: hostname:8080 + https: hostname:8443 + +- name: Set a proxy for specific protocols using a string + community.windows.win_http_proxy: + proxy: http=hostname:8080;https=hostname:8443 + bypass: server1,server2,<local> + +- name: Remove any proxy settings + community.windows.win_http_proxy: + proxy: '' + bypass: '' +''' + +RETURN = r''' +# +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_iis_virtualdirectory.ps1 b/ansible_collections/community/windows/plugins/modules/win_iis_virtualdirectory.ps1 new file mode 100644 index 000000000..d2c3ba07b --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_iis_virtualdirectory.ps1 @@ -0,0 +1,139 @@ +#!powershell + +# Copyright: (c) 2015, Henrik Wallström <henrik@wallstroms.nu> +# 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 +$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true +$site = Get-AnsibleParam -obj $params -name "site" -type "str" -failifempty $true +$application = Get-AnsibleParam -obj $params -name "application" -type "str" +$physical_path = Get-AnsibleParam -obj $params -name "physical_path" -type "str" +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent", "present" +$connect_as = Get-AnsibleParam -obj $params -name 'connect_as' -type 'str' -validateset 'specific_user', 'pass_through' +$username = Get-AnsibleParam -obj $params -name "username" -type "str" -failifempty ($connect_as -eq 'specific_user') +$password = Get-AnsibleParam -obj $params -name "password" -type "str" -failifempty ($connect_as -eq 'specific_user') + +# Ensure WebAdministration module is loaded +if ($null -eq (Get-Module "WebAdministration" -ErrorAction SilentlyContinue)) { + Import-Module WebAdministration +} + +# Result +$result = @{ + directory = @{} + changed = $false +} + +# Construct path +$directory_path = if ($application) { + "IIS:\Sites\$($site)\$($application)\$($name)" +} +else { + "IIS:\Sites\$($site)\$($name)" +} + +# Directory info +$directory = if ($application) { + Get-WebVirtualDirectory -Site $site -Name $name -Application $application +} +else { + Get-WebVirtualDirectory -Site $site -Name $name +} + +try { + # Add directory + If (($state -eq 'present') -and (-not $directory)) { + If (-not $physical_path) { + Fail-Json -obj $result -message "missing required arguments: physical_path" + } + If (-not (Test-Path -LiteralPath $physical_path)) { + Fail-Json -obj $result -message "specified folder must already exist: physical_path" + } + + $directory_parameters = @{ + Site = $site + Name = $name + PhysicalPath = $physical_path + } + + If ($application) { + $directory_parameters.Application = $application + } + + $directory = New-WebVirtualDirectory @directory_parameters -Force + $result.changed = $true + } + + # Remove directory + If ($state -eq 'absent' -and $directory) { + Remove-Item -LiteralPath $directory_path -Recurse -Force + $result.changed = $true + } + + $directory = if ($application) { + Get-WebVirtualDirectory -Site $site -Name $name -Application $application + } + else { + Get-WebVirtualDirectory -Site $site -Name $name + } + + If ($directory) { + + # Change Physical Path if needed + if ($physical_path) { + If (-not (Test-Path -LiteralPath $physical_path)) { + Fail-Json -obj $result -message "specified folder must already exist: physical_path" + } + + $vdir_folder = Get-Item -LiteralPath $directory.PhysicalPath + $folder = Get-Item -LiteralPath $physical_path + If ($folder.FullName -ne $vdir_folder.FullName) { + Set-ItemProperty -LiteralPath $directory_path -name physicalPath -value $physical_path + $result.changed = $true + } + } + + # Change username or password if needed + if ($connect_as -eq 'pass_through') { + if ($directory.username -ne '') { + Clear-ItemProperty -LiteralPath $directory_path -Name 'userName' + $result.changed = $true + } + if ($directory.password -ne '') { + Clear-ItemProperty -LiteralPath $directory_path -Name 'password' + $result.changed = $true + } + } + elseif ($connect_as -eq 'specific_user') { + if ($directory.username -ne $username) { + Set-ItemProperty -LiteralPath $directory_path -Name 'userName' -Value $username + $result.changed = $true + } + if ($directory.password -ne $password) { + Set-ItemProperty -LiteralPath $directory_path -Name 'password' -Value $password + $result.changed = $true + } + } + } +} +catch { + Fail-Json $result $_.Exception.Message +} + +# Result +$directory = if ($application) { + Get-WebVirtualDirectory -Site $site -Name $name -Application $application +} +else { + Get-WebVirtualDirectory -Site $site -Name $name +} + +$result.directory = @{ + PhysicalPath = $directory.PhysicalPath +} + +Exit-Json -obj $result
\ No newline at end of file diff --git a/ansible_collections/community/windows/plugins/modules/win_iis_virtualdirectory.py b/ansible_collections/community/windows/plugins/modules/win_iis_virtualdirectory.py new file mode 100644 index 000000000..5d572f8f7 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_iis_virtualdirectory.py @@ -0,0 +1,90 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2015, Henrik Wallström <henrik@wallstroms.nu> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_iis_virtualdirectory +short_description: Configures a virtual directory in IIS +description: + - Creates, Removes and configures a virtual directory in IIS. +options: + name: + description: + - The name of the virtual directory to create or remove. + type: str + required: yes + state: + description: + - Whether to add or remove the specified virtual directory. + - Removing will remove the virtual directory and all under it (Recursively). + type: str + choices: [ absent, present ] + default: present + site: + description: + - The site name under which the virtual directory is created or exists. + type: str + required: yes + application: + description: + - The application under which the virtual directory is created or exists. + type: str + physical_path: + description: + - The physical path to the folder in which the new virtual directory is created. + - The specified folder must already exist. + type: str + connect_as: + description: + - The type of authentication to use for the virtual directory. Either C(pass_through) or C(specific_user) + - If C(pass_through), IIS will use the identity of the user or application pool identity to access the physical path. + - If C(specific_user), IIS will use the credentials provided in I(username) and I(password) to access the physical path. + type: str + choices: [pass_through, specific_user] + version_added: 1.9.0 + username: + description: + - Specifies the user name of an account that can access configuration files and content for the virtual directory. + - Required when I(connect_as) is set to C(specific_user). + type: str + version_added: 1.9.0 + password: + description: + - The password associated with I(username). + - Required when I(connect_as) is set to C(specific_user). + type: str + version_added: 1.9.0 +seealso: +- module: community.windows.win_iis_webapplication +- module: community.windows.win_iis_webapppool +- module: community.windows.win_iis_webbinding +- module: community.windows.win_iis_website +author: +- Henrik Wallström (@henrikwallstrom) +''' + +EXAMPLES = r''' +- name: Create a virtual directory if it does not exist + community.windows.win_iis_virtualdirectory: + name: somedirectory + site: somesite + state: present + physical_path: C:\virtualdirectory\some + +- name: Remove a virtual directory if it exists + community.windows.win_iis_virtualdirectory: + name: somedirectory + site: somesite + state: absent + +- name: Create a virtual directory on an application if it does not exist + community.windows.win_iis_virtualdirectory: + name: somedirectory + site: somesite + application: someapp + state: present + physical_path: C:\virtualdirectory\some +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_iis_webapplication.ps1 b/ansible_collections/community/windows/plugins/modules/win_iis_webapplication.ps1 new file mode 100644 index 000000000..86fa850cc --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_iis_webapplication.ps1 @@ -0,0 +1,140 @@ +#!powershell + +# Copyright: (c) 2015, Henrik Wallström <henrik@wallstroms.nu> +# 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 +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false + +$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true +$site = Get-AnsibleParam -obj $params -name "site" -type "str" -failifempty $true +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent", "present" +$physical_path = Get-AnsibleParam -obj $params -name "physical_path" -type "str" -aliases "path" +$application_pool = Get-AnsibleParam -obj $params -name "application_pool" -type "str" +$connect_as = Get-AnsibleParam -obj $params -name 'connect_as' -type 'str' -validateset 'specific_user', 'pass_through' +$username = Get-AnsibleParam -obj $params -name "username" -type "str" -failifempty ($connect_as -eq 'specific_user') +$password = Get-AnsibleParam -obj $params -name "password" -type "str" -failifempty ($connect_as -eq 'specific_user') + +$result = @{ + application_pool = $application_pool + changed = $false + physical_path = $physical_path +} + +# Ensure WebAdministration module is loaded +if ($null -eq (Get-Module "WebAdministration" -ErrorAction SilentlyContinue)) { + Import-Module WebAdministration +} + +# Application info +$application = Get-WebApplication -Site $site -Name $name +$website = Get-Website -Name $site + +# Set ApplicationPool to current if not specified +if (!$application_pool) { + $application_pool = $website.applicationPool +} + +try { + # Add application + if (($state -eq 'present') -and (-not $application)) { + if (-not $physical_path) { + Fail-Json $result "missing required arguments: path" + } + if (-not (Test-Path -LiteralPath $physical_path)) { + Fail-Json $result "specified folder must already exist: path" + } + + $application_parameters = @{ + Name = $name + PhysicalPath = $physical_path + Site = $site + } + + if ($application_pool) { + $application_parameters.ApplicationPool = $application_pool + } + + if (-not $check_mode) { + $application = New-WebApplication @application_parameters -Force + } + $result.changed = $true + } + + # Remove application + if ($state -eq 'absent' -and $application) { + $application = Remove-WebApplication -Site $site -Name $name -WhatIf:$check_mode + $result.changed = $true + } + + $application = Get-WebApplication -Site $site -Name $name + if ($application) { + + # Change Physical Path if needed + if ($physical_path) { + if (-not (Test-Path -LiteralPath $physical_path)) { + Fail-Json $result "specified folder must already exist: path" + } + + $folder = Get-Item -LiteralPath $physical_path + if ($folder.FullName -ne $application.PhysicalPath) { + Set-ItemProperty -LiteralPath "IIS:\Sites\$($site)\$($name)" -name physicalPath -value $physical_path -WhatIf:$check_mode + $result.changed = $true + } + } + + # Change Application Pool if needed + if ($application_pool) { + if ($application_pool -ne $application.applicationPool) { + Set-ItemProperty -LiteralPath "IIS:\Sites\$($site)\$($name)" -name applicationPool -value $application_pool -WhatIf:$check_mode + $result.changed = $true + } + } + + # Change username and password if needed + $app_user = Get-ItemProperty -LiteralPath "IIS:\Sites\$($site)\$($name)" -Name 'userName' + $app_pass = Get-ItemProperty -LiteralPath "IIS:\Sites\$($site)\$($name)" -Name 'password' + if ($connect_as -eq 'pass_through') { + if ($app_user -ne '') { + Clear-ItemProperty -LiteralPath "IIS:\Sites\$($site)\$($name)" -Name 'userName' -WhatIf:$check_mode + $result.changed = $true + } + if ($app_pass -ne '') { + Clear-ItemProperty -LiteralPath "IIS:\Sites\$($site)\$($name)" -Name 'password' -WhatIf:$check_mode + $result.changed = $true + } + } + elseif ($connect_as -eq 'specific_user') { + if ($app_user -ne $username) { + Set-ItemProperty -LiteralPath "IIS:\Sites\$($site)\$($name)" -Name 'userName' -Value $username -WhatIf:$check_mode + $result.changed = $true + } + if ($app_pass -ne $password) { + Set-ItemProperty -LiteralPath "IIS:\Sites\$($site)\$($name)" -Name 'password' -Value $password -WhatIf:$check_mode + $result.changed = $true + } + } + } +} +catch { + Fail-Json $result $_.Exception.Message +} + +# When in check-mode or on removal, this may fail +$application = Get-WebApplication -Site $site -Name $name +if ($application) { + $app_user = Get-ItemProperty -LiteralPath "IIS:\Sites\$($site)\$($name)" -Name 'userName' + if ($app_user -eq '') { + $result.connect_as = 'pass_through' + } + else { + $result.connect_as = 'specific_user' + } + + $result.physical_path = $application.PhysicalPath + $result.application_pool = $application.ApplicationPool +} + +Exit-Json $result diff --git a/ansible_collections/community/windows/plugins/modules/win_iis_webapplication.py b/ansible_collections/community/windows/plugins/modules/win_iis_webapplication.py new file mode 100644 index 000000000..50149da7f --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_iis_webapplication.py @@ -0,0 +1,91 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2015, Henrik Wallström <henrik@wallstroms.nu> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_iis_webapplication +short_description: Configures IIS web applications +description: +- Creates, removes, and configures IIS web applications. +options: + name: + description: + - Name of the web application. + type: str + required: yes + site: + description: + - Name of the site on which the application is created. + type: str + required: yes + state: + description: + - State of the web application. + type: str + choices: [ absent, present ] + default: present + physical_path: + description: + - The physical path on the remote host to use for the new application. + - The specified folder must already exist. + type: str + application_pool: + description: + - The application pool in which the new site executes. + - If not specified, the application pool of the current website will be used. + type: str + connect_as: + description: + - The type of authentication to use for this application. Either C(pass_through) or C(specific_user) + - If C(pass_through), IIS will use the identity of the user or application pool identity to access the file system or network. + - If C(specific_user), IIS will use the credentials provided in I(username) and I(password) to access the file system or network. + type: str + choices: [pass_through, specific_user] + username: + description: + - Specifies the user name of an account that can access configuration files and content for this application. + - Required when I(connect_as) is set to C(specific_user). + type: str + password: + description: + - The password associated with I(username). + - Required when I(connect_as) is set to C(specific_user). + type: str +seealso: +- module: community.windows.win_iis_virtualdirectory +- module: community.windows.win_iis_webapppool +- module: community.windows.win_iis_webbinding +- module: community.windows.win_iis_website +author: +- Henrik Wallström (@henrikwallstrom) +''' + +EXAMPLES = r''' +- name: Add ACME webapplication on IIS. + community.windows.win_iis_webapplication: + name: api + site: acme + state: present + physical_path: C:\apps\acme\api +''' + +RETURN = r''' +application_pool: + description: The used/implemented application_pool value. + returned: success + type: str + sample: DefaultAppPool +physical_path: + description: The used/implemented physical_path value. + returned: success + type: str + sample: C:\apps\acme\api +connect_as: + description: How IIS will try to authenticate to the physical_path. + returned: when the application exists + type: str + sample: specific_user +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_iis_webapppool.ps1 b/ansible_collections/community/windows/plugins/modules/win_iis_webapppool.ps1 new file mode 100644 index 000000000..28b339b4e --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_iis_webapppool.ps1 @@ -0,0 +1,341 @@ +#!powershell + +# Copyright: (c) 2015, Henrik Wallström <henrik@wallstroms.nu> +# 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 -arguments $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 "str" -failifempty $true +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateSet "started", "restarted", "stopped", "absent", "present" +$result = @{ + changed = $false + attributes = @{} + info = @{ + name = $name + state = $state + attributes = @{} + cpu = @{} + failure = @{} + processModel = @{} + recycling = @{ + periodicRestart = @{} + } + } +} + +# Stores the free form attributes for the module +$attributes = @{} +$input_attributes = Get-AnsibleParam -obj $params -name "attributes" +if ($input_attributes) { + if ($input_attributes -is [System.Collections.Hashtable]) { + # Uses dict style parameters, newer and recommended style + $attributes = $input_attributes + } + else { + Fail-Json -obj $result -message "Using a string for the attributes parameter is not longer supported, please use a dict instead" + } +} +$result.attributes = $attributes + +Function Get-DotNetClassForAttribute($attribute_parent) { + switch ($attribute_parent) { + "attributes" { [Microsoft.Web.Administration.ApplicationPool] } + "cpu" { [Microsoft.Web.Administration.ApplicationPoolCpu] } + "failure" { [Microsoft.Web.Administration.ApplicationPoolFailure] } + "processModel" { [Microsoft.Web.Administration.ApplicationPoolProcessModel] } + "recycling" { [Microsoft.Web.Administration.ApplicationPoolRecycling] } + default { [Microsoft.Web.Administration.ApplicationPool] } + } +} + +Function Convert-CollectionToList($collection) { + $list = @() + + if ($collection -is [String]) { + $raw_list = $collection -split "," + foreach ($entry in $raw_list) { + $list += $entry.Trim() + } + } + elseif ($collection -is [Microsoft.IIs.PowerShell.Framework.ConfigurationElement]) { + # the collection is the value from IIS itself, we need to conver accordingly + foreach ($entry in $collection.Collection) { + $list += $entry.Value.ToString() + } + } + elseif ($collection -isnot [Array]) { + $list += $collection + } + else { + $list = $collection + } + + return , $list +} + +Function Compare-Value($current, $new) { + if ($null -eq $current) { + return $true + } + + if ($current -is [Array]) { + if ($new -isnot [Array]) { + return $true + } + + if ($current.Count -ne $new.Count) { + return $true + } + for ($i = 0; $i -lt $current.Count; $i++) { + if ($current[$i] -ne $new[$i]) { + return $true + } + } + } + else { + if ($current -ne $new) { + return $true + } + } + return $false +} + +Function Convert-ToPropertyValue($pool, $attribute_key, $attribute_value) { + # Will convert the new value to the enum value expected and cast accordingly to the type + if ([bool]($attribute_value.PSobject.Properties -match "Value")) { + $attribute_value = $attribute_value.Value + } + $attribute_key_split = $attribute_key -split "\." + if ($attribute_key_split.Length -eq 1) { + $attribute_parent = "attributes" + $attribute_child = $attribute_key + $attribute_meta = $pool.Attributes | Where-Object { $_.Name -eq $attribute_child } + } + elseif ($attribute_key_split.Length -gt 1) { + $attribute_parent = $attribute_key_split[0] + $attribute_key_split = $attribute_key_split[1..$($attribute_key_split.Length - 1)] + $parent = $pool.$attribute_parent + + foreach ($key in $attribute_key_split) { + $attribute_meta = $parent.Attributes | Where-Object { $_.Name -eq $key } + $parent = $parent.$key + if ($null -eq $attribute_meta) { + $attribute_meta = $parent + } + } + $attribute_child = $attribute_key_split[-1] + } + + if ($attribute_meta) { + if (($attribute_meta.PSObject.Properties.Name -eq "Collection").Count -gt 0) { + return , (Convert-CollectionToList -collection $attribute_value) + } + $type = $attribute_meta.Schema.Type + $value = $attribute_value + if ($type -eq "enum") { + # Attempt to convert the value from human friendly to enum value - use existing value if we fail + $dot_net_class = Get-DotNetClassForAttribute -attribute_parent $attribute_parent + $enum_attribute_name = $attribute_child.Substring(0, 1).ToUpper() + $attribute_child.Substring(1) + $enum = $dot_net_class.GetProperty($enum_attribute_name).PropertyType.FullName + if ($enum) { + $enum_values = [Enum]::GetValues($enum) + foreach ($enum_value in $enum_values) { + if ($attribute_value.GetType() -is $enum_value.GetType()) { + if ($enum_value -eq $attribute_value) { + $value = $enum_value + break + } + } + else { + if ([System.String]$enum_value -eq [System.String]$attribute_value) { + $value = $enum_value + break + } + } + } + } + } + # Try and cast the variable using the chosen type, revert to the default if it fails + Set-Variable -Name casted_value -Value ($value -as ([type] $attribute_meta.TypeName)) + if ($null -eq $casted_value) { + $value + } + else { + $casted_value + } + } + else { + $attribute_value + } +} + +# Ensure WebAdministration module is loaded +if ($null -eq (Get-Module -Name "WebAdministration" -ErrorAction SilentlyContinue)) { + Import-Module WebAdministration + $web_admin_dll_path = Join-Path $env:SystemRoot system32\inetsrv\Microsoft.Web.Administration.dll + Add-Type -LiteralPath $web_admin_dll_path +} + +$pool = Get-Item -LiteralPath IIS:\AppPools\$name -ErrorAction SilentlyContinue +if ($state -eq "absent") { + # Remove pool if present + if ($pool) { + try { + Remove-WebAppPool -Name $name -WhatIf:$check_mode + } + catch { + Fail-Json $result "Failed to remove Web App pool $($name): $($_.Exception.Message)" + } + $result.changed = $true + } +} +else { + # Add pool if absent + if (-not $pool) { + if (-not $check_mode) { + try { + New-WebAppPool -Name $name > $null + } + catch { + Fail-Json $result "Failed to create new Web App Pool $($name): $($_.Exception.Message)" + } + } + $result.changed = $true + # If in check mode this pool won't actually exists so skip it + if (-not $check_mode) { + $pool = Get-Item -LiteralPath IIS:\AppPools\$name + } + } + + # Cannot run the below in check mode if the pool did not always exist + if ($pool) { + # Modify pool based on parameters + foreach ($attribute in $attributes.GetEnumerator()) { + $attribute_key = $attribute.Name + $new_raw_value = $attribute.Value + $new_value = Convert-ToPropertyValue -pool $pool -attribute_key $attribute_key -attribute_value $new_raw_value + + $current_raw_value = Get-ItemProperty -LiteralPath IIS:\AppPools\$name -Name $attribute_key -ErrorAction SilentlyContinue + $current_value = Convert-ToPropertyValue -pool $pool -attribute_key $attribute_key -attribute_value $current_raw_value + + $changed = Compare-Value -current $current_value -new $new_value + if ($changed -eq $true) { + if ($new_value -is [Array]) { + try { + Clear-ItemProperty -LiteralPath IIS:\AppPools\$name -Name $attribute_key -WhatIf:$check_mode + } + catch { + $msg = -join @( + "Failed to clear attribute to Web App Pool $name. Attribute: $attribute_key, " + "Exception: $($_.Exception.Message)" + ) + Fail-Json -obj $result -message $msg + } + foreach ($value in $new_value) { + try { + New-ItemProperty -LiteralPath IIS:\AppPools\$name -Name $attribute_key -Value @{value = $value } -WhatIf:$check_mode > $null + } + catch { + $msg = -join @( + "Failed to add new attribute to Web App Pool $name. Attribute: $attribute_key, " + "Value: $value, Exception: $($_.Exception.Message)" + ) + Fail-Json -obj $result -message $msg + } + } + } + else { + try { + Set-ItemProperty -LiteralPath IIS:\AppPools\$name -Name $attribute_key -Value $new_value -WhatIf:$check_mode + } + catch { + $msg = -join @( + "Failed to set attribute to Web App Pool $name. Attribute: $attribute_key, " + "Value: $new_value, Exception: $($_.Exception.Message)" + ) + Fail-Json $result $msg + } + } + $result.changed = $true + } + } + + # Set the state of the pool + if ($pool.State -eq "Stopped") { + if ($state -eq "started" -or $state -eq "restarted") { + if (-not $check_mode) { + try { + Start-WebAppPool -Name $name > $null + } + catch { + Fail-Json $result "Failed to start Web App Pool $($name): $($_.Exception.Message)" + } + } + $result.changed = $true + } + } + else { + if ($state -eq "stopped") { + if (-not $check_mode) { + try { + Stop-WebAppPool -Name $name > $null + } + catch { + Fail-Json $result "Failed to stop Web App Pool $($name): $($_.Exception.Message)" + } + } + $result.changed = $true + } + elseif ($state -eq "restarted") { + if (-not $check_mode) { + try { + Restart-WebAppPool -Name $name > $null + } + catch { + Fail-Json $result "Failed to restart Web App Pool $($name): $($_.Exception.Message)" + } + } + $result.changed = $true + } + } + } +} + +# Get all the current attributes for the pool +$pool = Get-Item -LiteralPath IIS:\AppPools\$name -ErrorAction SilentlyContinue +$elements = @("attributes", "cpu", "failure", "processModel", "recycling") + +foreach ($element in $elements) { + if ($element -eq "attributes") { + $attribute_collection = $pool.Attributes + $attribute_parent = $pool + } + else { + $attribute_collection = $pool.$element.Attributes + $attribute_parent = $pool.$element + } + + foreach ($attribute in $attribute_collection) { + $attribute_name = $attribute.Name + if ($attribute_name -notlike "*password*") { + $attribute_value = $attribute_parent.$attribute_name + + $result.info.$element.Add($attribute_name, $attribute_value) + } + } +} + +# Manually get the periodicRestart attributes in recycling +foreach ($attribute in $pool.recycling.periodicRestart.Attributes) { + $attribute_name = $attribute.Name + $attribute_value = $pool.recycling.periodicRestart.$attribute_name + $result.info.recycling.periodicRestart.Add($attribute_name, $attribute_value) +} + +Exit-Json $result diff --git a/ansible_collections/community/windows/plugins/modules/win_iis_webapppool.py b/ansible_collections/community/windows/plugins/modules/win_iis_webapppool.py new file mode 100644 index 000000000..7cca2e3bf --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_iis_webapppool.py @@ -0,0 +1,206 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2015, Henrik Wallström <henrik@wallstroms.nu> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_iis_webapppool +short_description: Configure IIS Web Application Pools +description: + - Creates, removes and configures an IIS Web Application Pool. +options: + attributes: + description: + - This field is a free form dictionary value for the application pool + attributes. + - These attributes are based on the naming standard at + U(https://www.iis.net/configreference/system.applicationhost/applicationpools/add#005), + see the examples section for more details on how to set this. + - You can also set the attributes of child elements like cpu and + processModel, see the examples to see how it is done. + - While you can use the numeric values for enums it is recommended to use + the enum name itself, e.g. use SpecificUser instead of 3 for + processModel.identityType. + - managedPipelineMode may be either "Integrated" or "Classic". + - startMode may be either "OnDemand" or "AlwaysRunning". + - Use C(state) module parameter to modify the state of the app pool. + - When trying to set 'processModel.password' and you receive a 'Value + does fall within the expected range' error, you have a corrupted + keystore. Please follow + U(http://structuredsight.com/2014/10/26/im-out-of-range-youre-out-of-range/) + to help fix your host. + name: + description: + - Name of the application pool. + type: str + required: yes + state: + description: + - The state of the application pool. + - If C(absent) will ensure the app pool is removed. + - If C(present) will ensure the app pool is configured and exists. + - If C(restarted) will ensure the app pool exists and will restart, this + is never idempotent. + - If C(started) will ensure the app pool exists and is started. + - If C(stopped) will ensure the app pool exists and is stopped. + type: str + choices: [ absent, present, restarted, started, stopped ] + default: present +seealso: +- module: community.windows.win_iis_virtualdirectory +- module: community.windows.win_iis_webapplication +- module: community.windows.win_iis_webbinding +- module: community.windows.win_iis_website +author: +- Henrik Wallström (@henrikwallstrom) +- Jordan Borean (@jborean93) +''' + +EXAMPLES = r''' +- name: Return information about an existing application pool + community.windows.win_iis_webapppool: + name: DefaultAppPool + state: present + +- name: Create a new application pool in 'Started' state + community.windows.win_iis_webapppool: + name: AppPool + state: started + +- name: Stop an application pool + community.windows.win_iis_webapppool: + name: AppPool + state: stopped + +- name: Restart an application pool (non-idempotent) + community.windows.win_iis_webapppool: + name: AppPool + state: restarted + +- name: Change application pool attributes using new dict style + community.windows.win_iis_webapppool: + name: AppPool + attributes: + managedRuntimeVersion: v4.0 + autoStart: no + +- name: Creates an application pool, sets attributes and starts it + community.windows.win_iis_webapppool: + name: AnotherAppPool + state: started + attributes: + managedRuntimeVersion: v4.0 + autoStart: no + +# In the below example we are setting attributes in child element processModel +# https://www.iis.net/configreference/system.applicationhost/applicationpools/add/processmodel +- name: Manage child element and set identity of application pool + community.windows.win_iis_webapppool: + name: IdentitiyAppPool + state: started + attributes: + managedPipelineMode: Classic + processModel.identityType: SpecificUser + processModel.userName: '{{ansible_user}}' + processModel.password: '{{ansible_password}}' + processModel.loadUserProfile: true + +- name: Manage a timespan attribute + community.windows.win_iis_webapppool: + name: TimespanAppPool + state: started + attributes: + # Timespan with full string "day:hour:minute:second.millisecond" + recycling.periodicRestart.time: "00:00:05:00.000000" + recycling.periodicRestart.schedule: ["00:10:00", "05:30:00"] + # Shortened timespan "hour:minute:second" + processModel.pingResponseTime: "00:03:00" +''' + +RETURN = r''' +attributes: + description: Application Pool attributes that were set and processed by this + module invocation. + returned: success + type: dict + sample: + enable32BitAppOnWin64: "true" + managedRuntimeVersion: "v4.0" + managedPipelineMode: "Classic" +info: + description: Information on current state of the Application Pool. See + https://www.iis.net/configreference/system.applicationhost/applicationpools/add#005 + for the full list of return attributes based on your IIS version. + returned: success + type: complex + sample: + contains: + attributes: + description: Key value pairs showing the current Application Pool attributes. + returned: success + type: dict + sample: + autoStart: true + managedRuntimeLoader: "webengine4.dll" + managedPipelineMode: "Classic" + name: "DefaultAppPool" + CLRConfigFile: "" + passAnonymousToken: true + applicationPoolSid: "S-1-5-82-1352790163-598702362-1775843902-1923651883-1762956711" + queueLength: 1000 + managedRuntimeVersion: "v4.0" + state: "Started" + enableConfigurationOverride: true + startMode: "OnDemand" + enable32BitAppOnWin64: true + cpu: + description: Key value pairs showing the current Application Pool cpu attributes. + returned: success + type: dict + sample: + action: "NoAction" + limit: 0 + resetInterval: + Days: 0 + Hours: 0 + failure: + description: Key value pairs showing the current Application Pool failure attributes. + returned: success + type: dict + sample: + autoShutdownExe: "" + orphanActionExe: "" + rapidFailProtextionInterval: + Days: 0 + Hours: 0 + name: + description: Name of Application Pool that was processed by this module invocation. + returned: success + type: str + sample: "DefaultAppPool" + processModel: + description: Key value pairs showing the current Application Pool processModel attributes. + returned: success + type: dict + sample: + identityType: "ApplicationPoolIdentity" + logonType: "LogonBatch" + pingInterval: + Days: 0 + Hours: 0 + recycling: + description: Key value pairs showing the current Application Pool recycling attributes. + returned: success + type: dict + sample: + disallowOverlappingRotation: false + disallowRotationOnConfigChange: false + logEventOnRecycle: "Time,Requests,Schedule,Memory,IsapiUnhealthy,OnDemand,ConfigChange,PrivateMemory" + state: + description: Current runtime state of the pool as the module completed. + returned: success + type: str + sample: "Started" +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_iis_webbinding.ps1 b/ansible_collections/community/windows/plugins/modules/win_iis_webbinding.ps1 new file mode 100644 index 000000000..b6073f4f1 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_iis_webbinding.ps1 @@ -0,0 +1,348 @@ +#!powershell + +# Copyright: (c) 2017, Noah Sparks <nsparks@outlook.com> +# Copyright: (c) 2015, Henrik Wallström <henrik@wallstroms.nu> +# 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 -arguments $args -supports_check_mode $true +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false + +$name = Get-AnsibleParam $params -name "name" -type str -failifempty $true -aliases 'website' +$state = Get-AnsibleParam $params "state" -default "present" -validateSet "present", "absent" +$host_header = Get-AnsibleParam $params -name "host_header" -type str +$protocol = Get-AnsibleParam $params -name "protocol" -type str -default 'http' +$port = Get-AnsibleParam $params -name "port" -default '80' +$ip = Get-AnsibleParam $params -name "ip" -default '*' +$certificateHash = Get-AnsibleParam $params -name "certificate_hash" -type str -default ([string]::Empty) +$certificateStoreName = Get-AnsibleParam $params -name "certificate_store_name" -type str -default ([string]::Empty) +$sslFlags = Get-AnsibleParam $params -name "ssl_flags" -default '0' -ValidateSet '0', '1', '2', '3' + +$result = @{ + changed = $false +} + +################# +### Functions ### +################# +function New-BindingInfo { + $ht = @{ + 'bindingInformation' = $args[0].bindingInformation + 'ip' = $args[0].bindingInformation.split(':')[0] + 'port' = [int]$args[0].bindingInformation.split(':')[1] + 'hostheader' = $args[0].bindingInformation.split(':')[2] + #'isDsMapperEnabled' = $args[0].isDsMapperEnabled + 'protocol' = $args[0].protocol + 'certificateStoreName' = $args[0].certificateStoreName + 'certificateHash' = $args[0].certificateHash + } + + #handle sslflag support + If ([version][System.Environment]::OSVersion.Version -lt [version]'6.2') { + $ht.sslFlags = 'not supported' + } + Else { + $ht.sslFlags = [int]$args[0].sslFlags + } + + Return $ht +} + +# Used instead of get-webbinding to ensure we always return a single binding +# We can't filter properly with get-webbinding...ex get-webbinding ip * returns all bindings +# pass it $binding_parameters hashtable +function Get-SingleWebBinding { + + Try { + $site_bindings = get-webbinding -name $args[0].name + } + Catch { + # 2k8r2 throws this error when you run get-webbinding with no bindings in iis + $msg = 'Cannot process argument because the value of argument "obj" is null. Change the value of argument "obj" to a non-null value' + If (-not $_.Exception.Message.CompareTo($msg)) { + Throw $_.Exception.Message + } + Else { return } + } + + Foreach ($binding in $site_bindings) { + $splits = $binding.bindingInformation -split ':' + + if ( + $args[0].protocol -eq $binding.protocol -and + $args[0].ipaddress -eq $splits[0] -and + $args[0].port -eq $splits[1] -and + $args[0].hostheader -eq $splits[2] + ) { + Return $binding + } + } +} + + +############################# +### Pre-Action Validation ### +############################# +$os_version = [version][System.Environment]::OSVersion.Version + +# Ensure WebAdministration module is loaded +If ($os_version -lt [version]'6.1') { + Try { + Add-PSSnapin WebAdministration + } + Catch { + Fail-Json -obj $result -message "The WebAdministration snap-in is not present. Please make sure it is installed." + } +} +Else { + Try { + Import-Module WebAdministration + } + Catch { + Fail-Json -obj $result -message "Failed to load WebAdministration module. Is IIS installed? $($_.Exception.Message)" + } +} + +# ensure website targetted exists. -Name filter doesn't work on 2k8r2 so do where-object instead +$website_check = get-website | Where-Object { $_.name -eq $name } +If (-not $website_check) { + Fail-Json -obj $result -message "Unable to retrieve website with name $Name. Make sure the website name is valid and exists." +} + +# if OS older than 2012 (6.2) and ssl flags are set, fail. Otherwise toggle sni_support +If ($os_version -lt [version]'6.2') { + If ($sslFlags -ne 0) { + Fail-Json -obj $result -message "SNI and Certificate Store support is not available for systems older than 2012 (6.2)" + } + $sni_support = $false #will cause the sslflags check later to skip +} +Else { + $sni_support = $true +} + +# make sure ssl flags only specified with https protocol +If ($protocol -ne 'https' -and $sslFlags -gt 0) { + Fail-Json -obj $result -message "SSLFlags can only be set for HTTPS protocol" +} + +# validate certificate details if provided +# we don't do anything with cert on state: absent, so only validate present +If ($certificateHash -and $state -eq 'present') { + If ($protocol -ne 'https') { + Fail-Json -obj $result -message "You can only provide a certificate thumbprint when protocol is set to https" + } + + #apply default for cert store name + If (-Not $certificateStoreName) { + $certificateStoreName = 'my' + } + + #validate cert path + $cert_path = "cert:\LocalMachine\$certificateStoreName\$certificateHash" + If (-Not (Test-Path -LiteralPath $cert_path) ) { + Fail-Json -obj $result -message "Unable to locate certificate at $cert_path" + } +} + +# make sure binding info is valid for central cert store if sslflags -gt 1 +If ($sslFlags -gt 1 -and ($certificateHash -ne [string]::Empty -or $certificateStoreName -ne [string]::Empty)) { + Fail-Json -obj $result -message "You set sslFlags to $sslFlags. This indicates you wish to use the Central Certificate Store feature. + This cannot be used in combination with certficiate_hash and certificate_store_name. When using the Central Certificate Store feature, + the certificate is automatically retrieved from the store rather than manually assigned to the binding." +} + +# disallow host_header: '*' +If ($host_header -eq '*') { + Fail-Json -obj $result -message "To make or remove a catch-all binding, please omit the host_header parameter entirely rather than specify host_header *" +} + +########################## +### start action items ### +########################## + +# create binding search splat +$binding_parameters = @{ + Name = $name + Protocol = $protocol + Port = $port + IPAddress = $ip +} + +# insert host header to search if specified, otherwise it will return * (all bindings matching protocol/ip) +If ($host_header) { + $binding_parameters.HostHeader = $host_header +} +Else { + $binding_parameters.HostHeader = [string]::Empty +} + +# Get bindings matching parameters +Try { + $current_bindings = Get-SingleWebBinding $binding_parameters +} +Catch { + Fail-Json -obj $result -message "Failed to retrieve bindings with Get-SingleWebBinding - $($_.Exception.Message)" +} + +################################################ +### Remove binding or exit if already absent ### +################################################ +If ($current_bindings -and $state -eq 'absent') { + Try { + #there is a bug in this method that will result in all bindings being removed if the IP in $current_bindings is a * + #$current_bindings | Remove-WebBinding -verbose -WhatIf:$check_mode + + #another method that did not work. It kept failing to match on element and removed everything. + #$element = @{protocol="$protocol";bindingInformation="$ip`:$port`:$host_header"} + #Remove-WebconfigurationProperty -filter $current_bindings.ItemXPath -Name Bindings.collection -AtElement $element -WhatIf #:$check_mode + + #this method works + [array]$bindings = Get-WebconfigurationProperty -filter $current_bindings.ItemXPath -Name Bindings.collection + + $index = Foreach ($item in $bindings) { + If ( $protocol -eq $item.protocol -and $current_bindings.bindingInformation -eq $item.bindingInformation ) { + $bindings.indexof($item) + break + } + } + + Remove-WebconfigurationProperty -filter $current_bindings.ItemXPath -Name Bindings.collection -AtIndex $index -WhatIf:$check_mode + $result.changed = $true + } + + Catch { + Fail-Json -obj $result -message "Failed to remove the binding from IIS - $($_.Exception.Message)" + } + + # removing bindings from iis may not also remove them from iis:\sslbindings + + $result.operation_type = 'removed' + $result.binding_info = $current_bindings | ForEach-Object { New-BindingInfo $_ } + Exit-Json -obj $result +} +ElseIf (-Not $current_bindings -and $state -eq 'absent') { + # exit changed: false since it's already gone + Exit-Json -obj $result +} + + +################################ +### Modify existing bindings ### +################################ +<# +since we have already have the parameters available to get-webbinding, +we just need to check here for the ones that are not available which are the +ssl settings (hash, store, sslflags). If they aren't set we update here, or +exit with changed: false +#> +ElseIf ($current_bindings) { + #ran into a strange edge case in testing where I was able to retrieve bindings but not expand all the properties + #when adding a self-signed wildcard cert to a binding. it seemed to permanently break the binding. only removing it + #would cause the error to stop. + Try { + $null = $current_bindings | Select-Object * + } + Catch { + $msg = -join @( + "Found a matching binding, but failed to expand it's properties (get-binding | FL *). " + "In testing, this was caused by using a self-signed wildcard certificate. $($_.Exception.Message)" + ) + Fail-Json -obj $result -message $msg + } + + # check if there is a match on the ssl parameters + If ( ($current_bindings.sslFlags -ne $sslFlags -and $sni_support) -or + $current_bindings.certificateHash -ne $certificateHash -or + $current_bindings.certificateStoreName -ne $certificateStoreName) { + # match/update SNI + If ($current_bindings.sslFlags -ne $sslFlags -and $sni_support) { + Try { + Set-WebBinding -Name $name -IPAddress $ip -Port $port -HostHeader $host_header -PropertyName sslFlags -value $sslFlags -whatif:$check_mode + $result.changed = $true + } + Catch { + Fail-Json -obj $result -message "Failed to update sslFlags on binding - $($_.Exception.Message)" + } + + # Refresh the binding object since it has been changed + Try { + $current_bindings = Get-SingleWebBinding $binding_parameters + } + Catch { + Fail-Json -obj $result -message "Failed to refresh bindings after setting sslFlags - $($_.Exception.Message)" + } + } + # match/update certificate + If ($current_bindings.certificateHash -ne $certificateHash -or $current_bindings.certificateStoreName -ne $certificateStoreName) { + If (-Not $check_mode) { + Try { + $current_bindings.AddSslCertificate($certificateHash, $certificateStoreName) + } + Catch { + Fail-Json -obj $result -message "Failed to set new SSL certificate - $($_.Exception.Message)" + } + } + } + $result.changed = $true + $result.operation_type = 'updated' + $result.website_state = (Get-Website | Where-Object { $_.Name -eq $Name }).State + $result.binding_info = New-BindingInfo (Get-SingleWebBinding $binding_parameters) + Exit-Json -obj $result #exit changed true + } + Else { + $result.operation_type = 'matched' + $result.website_state = (Get-Website | Where-Object { $_.Name -eq $Name }).State + $result.binding_info = New-BindingInfo (Get-SingleWebBinding $binding_parameters) + Exit-Json -obj $result #exit changed false + } +} + +######################## +### Add new bindings ### +######################## +ElseIf (-not $current_bindings -and $state -eq 'present') { + # add binding. this creates the binding, but does not apply a certificate to it. + Try { + If (-not $check_mode) { + If ($sni_support) { + New-WebBinding @binding_parameters -SslFlags $sslFlags -Force + } + Else { + New-WebBinding @binding_parameters -Force + } + } + $result.changed = $true + } + Catch { + $result.website_state = (Get-Website | Where-Object { $_.Name -eq $Name }).State + Fail-Json -obj $result -message "Failed at creating new binding (note: creating binding and adding ssl are separate steps) - $($_.Exception.Message)" + } + + # add certificate to binding + If ($certificateHash -and -not $check_mode) { + Try { + #$new_binding = get-webbinding -Name $name -IPAddress $ip -port $port -Protocol $protocol -hostheader $host_header + $new_binding = Get-SingleWebBinding $binding_parameters + $new_binding.addsslcertificate($certificateHash, $certificateStoreName) + } + Catch { + $result.website_state = (Get-Website | Where-Object { $_.Name -eq $Name }).State + Fail-Json -obj $result -message "Failed to set new SSL certificate - $($_.Exception.Message)" + } + } + + $result.changed = $true + $result.operation_type = 'added' + $result.website_state = (Get-Website | Where-Object { $_.Name -eq $Name }).State + + # incase there are no bindings we do a check before calling New-BindingInfo + $web_binding = Get-SingleWebBinding $binding_parameters + if ($web_binding) { + $result.binding_info = New-BindingInfo $web_binding + } + else { + $result.binding_info = $null + } + Exit-Json $result +} diff --git a/ansible_collections/community/windows/plugins/modules/win_iis_webbinding.py b/ansible_collections/community/windows/plugins/modules/win_iis_webbinding.py new file mode 100644 index 000000000..cf3d42b11 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_iis_webbinding.py @@ -0,0 +1,145 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2017, Noah Sparks <nsparks@outlook.com> +# Copyright: (c) 2017, Henrik Wallström <henrik@wallstroms.nu> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_iis_webbinding +short_description: Configures a IIS Web site binding +description: + - Creates, removes and configures a binding to an existing IIS Web site. +options: + name: + description: + - Names of web site. + type: str + required: yes + aliases: [ website ] + state: + description: + - State of the binding. + type: str + choices: [ absent, present ] + default: present + port: + description: + - The port to bind to / use for the new site. + type: int + default: 80 + ip: + description: + - The IP address to bind to / use for the new site. + type: str + default: '*' + host_header: + description: + - The host header to bind to / use for the new site. + - If you are creating/removing a catch-all binding, omit this parameter rather than defining it as '*'. + type: str + protocol: + description: + - The protocol to be used for the Web binding (usually HTTP, HTTPS, or FTP). + type: str + default: http + certificate_hash: + description: + - Certificate hash (thumbprint) for the SSL binding. The certificate hash is the unique identifier for the certificate. + type: str + certificate_store_name: + description: + - Name of the certificate store where the certificate for the binding is located. + type: str + default: my + ssl_flags: + description: + - This parameter is only valid on Server 2012 and newer. + - Primarily used for enabling and disabling server name indication (SNI). + - Set to C(0) to disable SNI. + - Set to C(1) to enable SNI. + type: str +seealso: +- module: community.windows.win_iis_virtualdirectory +- module: community.windows.win_iis_webapplication +- module: community.windows.win_iis_webapppool +- module: community.windows.win_iis_website +author: + - Noah Sparks (@nwsparks) + - Henrik Wallström (@henrikwallstrom) +''' + +EXAMPLES = r''' +- name: Add a HTTP binding on port 9090 + community.windows.win_iis_webbinding: + name: Default Web Site + port: 9090 + state: present + +- name: Remove the HTTP binding on port 9090 + community.windows.win_iis_webbinding: + name: Default Web Site + port: 9090 + state: absent + +- name: Remove the default http binding + community.windows.win_iis_webbinding: + name: Default Web Site + port: 80 + ip: '*' + state: absent + +- name: Add a HTTPS binding + community.windows.win_iis_webbinding: + name: Default Web Site + protocol: https + port: 443 + ip: 127.0.0.1 + certificate_hash: B0D0FA8408FC67B230338FCA584D03792DA73F4C + state: present + +- name: Add a HTTPS binding with host header and SNI enabled + community.windows.win_iis_webbinding: + name: Default Web Site + protocol: https + port: 443 + host_header: test.com + ssl_flags: 1 + certificate_hash: D1A3AF8988FD32D1A3AF8988FD323792DA73F4C + state: present +''' + +RETURN = r''' +website_state: + description: + - The state of the website being targetted + - Can be helpful in case you accidentally cause a binding collision + which can result in the targetted site being stopped + returned: always + type: str + sample: "Started" +operation_type: + description: + - The type of operation performed + - Can be removed, updated, matched, or added + returned: on success + type: str + sample: "removed" +binding_info: + description: + - Information on the binding being manipulated + returned: on success + type: dict + sample: |- + "binding_info": { + "bindingInformation": "127.0.0.1:443:", + "certificateHash": "FF3910CE089397F1B5A77EB7BAFDD8F44CDE77DD", + "certificateStoreName": "MY", + "hostheader": "", + "ip": "127.0.0.1", + "port": 443, + "protocol": "https", + "sslFlags": "not supported" + } +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_iis_website.ps1 b/ansible_collections/community/windows/plugins/modules/win_iis_website.ps1 new file mode 100644 index 000000000..571fb3d56 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_iis_website.ps1 @@ -0,0 +1,175 @@ +#!powershell + +# Copyright: (c) 2015, Henrik Wallström <henrik@wallstroms.nu> +# 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 +$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true +$application_pool = Get-AnsibleParam -obj $params -name "application_pool" -type "str" +$physical_path = Get-AnsibleParam -obj $params -name "physical_path" -type "str" +$site_id = Get-AnsibleParam -obj $params -name "site_id" -type "str" +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -validateset "absent", "restarted", "started", "stopped" + +# Binding Parameters +$bind_port = Get-AnsibleParam -obj $params -name "port" -type "int" +$bind_ip = Get-AnsibleParam -obj $params -name "ip" -type "str" +$bind_hostname = Get-AnsibleParam -obj $params -name "hostname" -type "str" + +# Custom site Parameters from string where properties +# are separated by a pipe and property name/values by colon. +# Ex. "foo:1|bar:2" +$parameters = Get-AnsibleParam -obj $params -name "parameters" -type "str" +if ($null -ne $parameters) { + $parameters = @($parameters -split '\|' | ForEach-Object { + return , ($_ -split "\:", 2) + }) +} + + +# Ensure WebAdministration module is loaded +if ($null -eq (Get-Module "WebAdministration" -ErrorAction SilentlyContinue)) { + Import-Module WebAdministration +} + +# Result +$result = @{ + site = @{} + changed = $false +} + +# Site info +$site = Get-Website | Where-Object { $_.Name -eq $name } + +Try { + # Add site + If (($state -ne 'absent') -and (-not $site)) { + If (-not $physical_path) { + Fail-Json -obj $result -message "missing required arguments: physical_path" + } + ElseIf (-not (Test-Path -LiteralPath $physical_path)) { + Fail-Json -obj $result -message "specified folder must already exist: physical_path" + } + + $site_parameters = @{ + Name = $name + PhysicalPath = $physical_path + } + + If ($application_pool) { + $site_parameters.ApplicationPool = $application_pool + } + + If ($site_id) { + $site_parameters.ID = $site_id + } + + If ($bind_port) { + $site_parameters.Port = $bind_port + } + + If ($bind_ip) { + $site_parameters.IPAddress = $bind_ip + } + + If ($bind_hostname) { + $site_parameters.HostHeader = $bind_hostname + } + + # Fix for error "New-Item : Index was outside the bounds of the array." + # This is a bug in the New-WebSite commandlet. Apparently there must be at least one site configured in IIS otherwise New-WebSite crashes. + # For more details, see http://stackoverflow.com/questions/3573889/ps-c-new-website-blah-throws-index-was-outside-the-bounds-of-the-array + $sites_list = Get-ChildItem -LiteralPath IIS:\sites + if ($null -eq $sites_list) { + if ($site_id) { + $site_parameters.ID = $site_id + } + else { + $site_parameters.ID = 1 + } + } + + $site = New-Website @site_parameters -Force + $result.changed = $true + } + + # Remove site + If ($state -eq 'absent' -and $site) { + $site = Remove-Website -Name $name + $result.changed = $true + } + + $site = Get-Website | Where-Object { $_.Name -eq $name } + If ($site) { + # Change Physical Path if needed + if ($physical_path) { + If (-not (Test-Path -LiteralPath $physical_path)) { + Fail-Json -obj $result -message "specified folder must already exist: physical_path" + } + + $folder = Get-Item -LiteralPath $physical_path + If ($folder.FullName -ne $site.PhysicalPath) { + Set-ItemProperty -LiteralPath "IIS:\Sites\$($site.Name)" -name physicalPath -value $folder.FullName + $result.changed = $true + } + } + + # Change Application Pool if needed + if ($application_pool) { + If ($application_pool -ne $site.applicationPool) { + Set-ItemProperty -LiteralPath "IIS:\Sites\$($site.Name)" -name applicationPool -value $application_pool + $result.changed = $true + } + } + + # Set properties + if ($parameters) { + $parameters | ForEach-Object { + $property_value = Get-ItemProperty -LiteralPath "IIS:\Sites\$($site.Name)" $_[0] + + switch ($property_value.GetType().Name) { + "ConfigurationAttribute" { $parameter_value = $property_value.value } + "String" { $parameter_value = $property_value } + } + + if ((-not $parameter_value) -or ($parameter_value) -ne $_[1]) { + Set-ItemProperty -LiteralPath "IIS:\Sites\$($site.Name)" $_[0] $_[1] + $result.changed = $true + } + } + } + + # Set run state + if ((($state -eq 'stopped') -or ($state -eq 'restarted')) -and ($site.State -eq 'Started')) { + Stop-Website -Name $name -ErrorAction Stop + $result.changed = $true + } + if ((($state -eq 'started') -and ($site.State -eq 'Stopped')) -or ($state -eq 'restarted')) { + Start-Website -Name $name -ErrorAction Stop + $result.changed = $true + } + } +} +Catch { + Fail-Json -obj $result -message $_.Exception.Message +} + +if ($state -ne 'absent') { + $site = Get-Website | Where-Object { $_.Name -eq $name } +} + +if ($site) { + $result.site = @{ + Name = $site.Name + ID = $site.ID + State = $site.State + PhysicalPath = $site.PhysicalPath + ApplicationPool = $site.applicationPool + Bindings = @($site.Bindings.Collection | ForEach-Object { $_.BindingInformation }) + } +} + +Exit-Json -obj $result diff --git a/ansible_collections/community/windows/plugins/modules/win_iis_website.py b/ansible_collections/community/windows/plugins/modules/win_iis_website.py new file mode 100644 index 000000000..c1fe192d3 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_iis_website.py @@ -0,0 +1,130 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2015, Henrik Wallström <henrik@wallstroms.nu> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_iis_website +short_description: Configures a IIS Web site +description: + - Creates, Removes and configures a IIS Web site. +options: + name: + description: + - Names of web site. + type: str + required: yes + site_id: + description: + - Explicitly set the IIS numeric ID for a site. + - Note that this value cannot be changed after the website has been created. + type: str + state: + description: + - State of the web site + type: str + choices: [ absent, started, stopped, restarted ] + physical_path: + description: + - The physical path on the remote host to use for the new site. + - The specified folder must already exist. + type: str + application_pool: + description: + - The application pool in which the new site executes. + type: str + port: + description: + - The port to bind to / use for the new site. + type: int + ip: + description: + - The IP address to bind to / use for the new site. + type: str + hostname: + description: + - The host header to bind to / use for the new site. + type: str + parameters: + description: + - Custom site Parameters from string where properties are separated by a pipe and property name/values by colon Ex. "foo:1|bar:2" + - Some custom parameters that you can use are listed below, this isn't a definitive list but some common parameters. + - C(logfile.directory) - Physical path to store Logs, e.g. C(D:\IIS-LOGs\). + - C(logfile.period) - Log file rollover scheduled accepting these values, how frequently the log file should be rolled-over, + e.g. C(Hourly, Daily, Weekly, Monthly). + - C(logfile.LogFormat) - Log file format, by default IIS uses C(W3C). + - C(logfile.truncateSize) - The size at which the log file contents will be trunsted, expressed in bytes. + type: str +seealso: +- module: community.windows.win_iis_virtualdirectory +- module: community.windows.win_iis_webapplication +- module: community.windows.win_iis_webapppool +- module: community.windows.win_iis_webbinding +author: +- Henrik Wallström (@henrikwallstrom) +''' + +EXAMPLES = r''' + +# Start a website + +- name: Acme IIS site + community.windows.win_iis_website: + name: Acme + state: started + port: 80 + ip: 127.0.0.1 + hostname: acme.local + application_pool: acme + physical_path: C:\sites\acme + parameters: logfile.directory:C:\sites\logs + register: website + +# Remove Default Web Site and the standard port 80 binding +- name: Remove Default Web Site + community.windows.win_iis_website: + name: "Default Web Site" + state: absent + +# Create a WebSite with custom Logging configuration (Logs Location, Format and Rolling Over). + +- name: Creating WebSite with Custom Log location, Format 3WC and rolling over every hour. + community.windows.win_iis_website: + name: MyCustom_Web_Shop_Site + state: started + port: 80 + ip: '*' + hostname: '*' + physical_path: D:\wwwroot\websites\my-shop-site + parameters: logfile.directory:D:\IIS-LOGS\websites\my-shop-site|logfile.period:Hourly|logFile.logFormat:W3C + application_pool: my-shop-site + +# Some commandline examples: + +# This return information about an existing host +# $ ansible -i vagrant-inventory -m community.windows.win_iis_website -a "name='Default Web Site'" window +# host | success >> { +# "changed": false, +# "site": { +# "ApplicationPool": "DefaultAppPool", +# "Bindings": [ +# "*:80:" +# ], +# "ID": 1, +# "Name": "Default Web Site", +# "PhysicalPath": "%SystemDrive%\\inetpub\\wwwroot", +# "State": "Stopped" +# } +# } + +# This stops an existing site. +# $ ansible -i hosts -m community.windows.win_iis_website -a "name='Default Web Site' state=stopped" host + +# This creates a new site. +# $ ansible -i hosts -m community.windows.win_iis_website -a "name=acme physical_path=C:\\sites\\acme" host + +# Change logfile. +# $ ansible -i hosts -m community.windows.win_iis_website -a "name=acme physical_path=C:\\sites\\acme" host +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_inet_proxy.ps1 b/ansible_collections/community/windows/plugins/modules/win_inet_proxy.ps1 new file mode 100644 index 000000000..769a8f725 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_inet_proxy.ps1 @@ -0,0 +1,496 @@ +#!powershell + +# Copyright: (c) 2019, 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.AddType + +$spec = @{ + options = @{ + auto_detect = @{ type = "bool"; default = $true } + auto_config_url = @{ type = "str" } + proxy = @{ type = "raw" } + bypass = @{ type = "list"; elements = "str"; no_log = $false } + connection = @{ type = "str" } + } + required_by = @{ + bypass = @("proxy") + } + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$auto_detect = $module.Params.auto_detect +$auto_config_url = $module.Params.auto_config_url +$proxy = $module.Params.proxy +$bypass = $module.Params.bypass +$connection = $module.Params.connection + +# Parse the raw value, it should be a Dictionary or String +if ($proxy -is [System.Collections.IDictionary]) { + $valid_keys = [System.Collections.Generic.List`1[String]]@("http", "https", "ftp", "socks") + # Check to make sure we don't have any invalid keys in the dict + $invalid_keys = [System.Collections.Generic.List`1[String]]@() + foreach ($k in $proxy.Keys) { + if ($k -notin $valid_keys) { + $invalid_keys.Add($k) + } + } + + if ($invalid_keys.Count -gt 0) { + $invalid_keys = $invalid_keys | Sort-Object # So our test assertion doesn't fail due to random ordering + $module.FailJson("Invalid keys found in proxy: $($invalid_keys -join ', '). Valid keys are $($valid_keys -join ', ').") + } + + # Build the proxy string in the form 'protocol=host;', the order of valid_keys is also important + $proxy_list = [System.Collections.Generic.List`1[String]]@() + foreach ($k in $valid_keys) { + if ($proxy.ContainsKey($k)) { + $proxy_list.Add("$k=$($proxy.$k)") + } + } + $proxy = $proxy_list -join ";" +} +elseif ($null -ne $proxy) { + $proxy = $proxy.ToString() +} + +if ($bypass) { + if ([System.String]::IsNullOrEmpty($proxy)) { + $module.FailJson("missing parameter(s) required by ''bypass'': proxy") + } + $bypass = $bypass -join ';' +} + +$win_inet_invoke = @' +using Microsoft.Win32.SafeHandles; +using System; +using System.Collections.Generic; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; + +namespace Ansible.WinINetProxy +{ + internal class NativeHelpers + { + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public class INTERNET_PER_CONN_OPTION_LISTW : IDisposable + { + public UInt32 dwSize; + public IntPtr pszConnection; + public UInt32 dwOptionCount; + public UInt32 dwOptionError; + public IntPtr pOptions; + + public INTERNET_PER_CONN_OPTION_LISTW() + { + dwSize = (UInt32)Marshal.SizeOf(this); + } + + public void Dispose() + { + if (pszConnection != IntPtr.Zero) + Marshal.FreeHGlobal(pszConnection); + if (pOptions != IntPtr.Zero) + Marshal.FreeHGlobal(pOptions); + GC.SuppressFinalize(this); + } + ~INTERNET_PER_CONN_OPTION_LISTW() { this.Dispose(); } + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public class INTERNET_PER_CONN_OPTIONW : IDisposable + { + public INTERNET_PER_CONN_OPTION dwOption; + public ValueUnion Value; + + [StructLayout(LayoutKind.Explicit)] + public class ValueUnion + { + [FieldOffset(0)] + public UInt32 dwValue; + + [FieldOffset(0)] + public IntPtr pszValue; + + [FieldOffset(0)] + public System.Runtime.InteropServices.ComTypes.FILETIME ftValue; + } + + public void Dispose() + { + // We can't just check if Value.pszValue is not IntPtr.Zero as the union means it could be set even + // when the value is a UInt32 or FILETIME. We check against a known string option type and only free + // the value in those cases. + List<INTERNET_PER_CONN_OPTION> stringOptions = new List<INTERNET_PER_CONN_OPTION> + { + { INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_AUTOCONFIG_URL }, + { INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_BYPASS }, + { INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_SERVER } + }; + if (Value != null && Value.pszValue != IntPtr.Zero && stringOptions.Contains(dwOption)) + Marshal.FreeHGlobal(Value.pszValue); + GC.SuppressFinalize(this); + } + ~INTERNET_PER_CONN_OPTIONW() { this.Dispose(); } + } + + public enum INTERNET_OPTION : uint + { + INTERNET_OPTION_PER_CONNECTION_OPTION = 75, + INTERNET_OPTION_PROXY_SETTINGS_CHANGED = 95, + } + + public enum INTERNET_PER_CONN_OPTION : uint + { + INTERNET_PER_CONN_FLAGS = 1, + INTERNET_PER_CONN_PROXY_SERVER = 2, + INTERNET_PER_CONN_PROXY_BYPASS = 3, + INTERNET_PER_CONN_AUTOCONFIG_URL = 4, + INTERNET_PER_CONN_AUTODISCOVERY_FLAGS = 5, + INTERNET_PER_CONN_FLAGS_UI = 10, // IE8+ - Included with Windows 7 and Server 2008 R2 + } + + [Flags] + public enum PER_CONN_FLAGS : uint + { + PROXY_TYPE_DIRECT = 0x00000001, + PROXY_TYPE_PROXY = 0x00000002, + PROXY_TYPE_AUTO_PROXY_URL = 0x00000004, + PROXY_TYPE_AUTO_DETECT = 0x00000008, + } + } + + internal class NativeMethods + { + [DllImport("Wininet.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool InternetQueryOptionW( + IntPtr hInternet, + NativeHelpers.INTERNET_OPTION dwOption, + SafeMemoryBuffer lpBuffer, + ref UInt32 lpdwBufferLength); + + [DllImport("Wininet.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool InternetSetOptionW( + IntPtr hInternet, + NativeHelpers.INTERNET_OPTION dwOption, + SafeMemoryBuffer lpBuffer, + UInt32 dwBufferLength); + + [DllImport("Rasapi32.dll", CharSet = CharSet.Unicode)] + public static extern UInt32 RasValidateEntryNameW( + string lpszPhonebook, + string lpszEntry); + } + + internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeMemoryBuffer() : base(true) { } + public SafeMemoryBuffer(int cb) : base(true) + { + 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; + } + } + + 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 WinINetProxy + { + private string Connection; + + public string AutoConfigUrl; + public bool AutoDetect; + public string Proxy; + public string ProxyBypass; + + public WinINetProxy(string connection) + { + Connection = connection; + Refresh(); + } + + public static bool IsValidConnection(string name) + { + // RasValidateEntryName is used to verify is a name can be a valid phonebook entry. It returns 0 if no + // entry exists and 183 if it already exists. We just need to check if it returns 183 to verify the + // connection name. + return NativeMethods.RasValidateEntryNameW(null, name) == 183; + } + + public void Refresh() + { + using (var connFlags = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_FLAGS_UI)) + using (var autoConfigUrl = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_AUTOCONFIG_URL)) + using (var server = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_SERVER)) + using (var bypass = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_BYPASS)) + { + NativeHelpers.INTERNET_PER_CONN_OPTIONW[] options = new NativeHelpers.INTERNET_PER_CONN_OPTIONW[] + { + connFlags, autoConfigUrl, server, bypass + }; + + try + { + QueryOption(options, Connection); + } + catch (Win32Exception e) + { + if (e.NativeErrorCode == 87) // ERROR_INVALID_PARAMETER + { + // INTERNET_PER_CONN_FLAGS_UI only works for IE8+, try the fallback in case we are still working + // with an ancient version. + connFlags.dwOption = NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_FLAGS; + QueryOption(options, Connection); + } + else + throw; + } + + NativeHelpers.PER_CONN_FLAGS flags = (NativeHelpers.PER_CONN_FLAGS)connFlags.Value.dwValue; + + AutoConfigUrl = flags.HasFlag(NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_AUTO_PROXY_URL) + ? Marshal.PtrToStringUni(autoConfigUrl.Value.pszValue) : null; + AutoDetect = flags.HasFlag(NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_AUTO_DETECT); + if (flags.HasFlag(NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_PROXY)) + { + Proxy = Marshal.PtrToStringUni(server.Value.pszValue); + ProxyBypass = Marshal.PtrToStringUni(bypass.Value.pszValue); + } + else + { + Proxy = null; + ProxyBypass = null; + } + } + } + + public void Set() + { + using (var connFlags = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_FLAGS_UI)) + using (var autoConfigUrl = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_AUTOCONFIG_URL)) + using (var server = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_SERVER)) + using (var bypass = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_BYPASS)) + { + List<NativeHelpers.INTERNET_PER_CONN_OPTIONW> options = new List<NativeHelpers.INTERNET_PER_CONN_OPTIONW>(); + + // PROXY_TYPE_DIRECT seems to always be set, need to verify + NativeHelpers.PER_CONN_FLAGS flags = NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_DIRECT; + if (AutoDetect) + flags |= NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_AUTO_DETECT; + + if (!String.IsNullOrEmpty(AutoConfigUrl)) + { + flags |= NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_AUTO_PROXY_URL; + autoConfigUrl.Value.pszValue = Marshal.StringToHGlobalUni(AutoConfigUrl); + } + options.Add(autoConfigUrl); + + if (!String.IsNullOrEmpty(Proxy)) + { + flags |= NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_PROXY; + server.Value.pszValue = Marshal.StringToHGlobalUni(Proxy); + } + options.Add(server); + + if (!String.IsNullOrEmpty(ProxyBypass)) + bypass.Value.pszValue = Marshal.StringToHGlobalUni(ProxyBypass); + options.Add(bypass); + + connFlags.Value.dwValue = (UInt32)flags; + options.Add(connFlags); + + SetOption(options.ToArray(), Connection); + + // Tell IE that the proxy settings have been changed. + if (!NativeMethods.InternetSetOptionW( + IntPtr.Zero, + NativeHelpers.INTERNET_OPTION.INTERNET_OPTION_PROXY_SETTINGS_CHANGED, + new SafeMemoryBuffer(IntPtr.Zero), + 0)) + { + throw new Win32Exception("InternetSetOptionW(INTERNET_OPTION_PROXY_SETTINGS_CHANGED) failed"); + } + } + } + + internal static NativeHelpers.INTERNET_PER_CONN_OPTIONW CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION option) + { + return new NativeHelpers.INTERNET_PER_CONN_OPTIONW + { + dwOption = option, + Value = new NativeHelpers.INTERNET_PER_CONN_OPTIONW.ValueUnion(), + }; + } + + internal static void QueryOption(NativeHelpers.INTERNET_PER_CONN_OPTIONW[] options, string connection = null) + { + using (NativeHelpers.INTERNET_PER_CONN_OPTION_LISTW optionList = new NativeHelpers.INTERNET_PER_CONN_OPTION_LISTW()) + using (SafeMemoryBuffer optionListPtr = MarshalOptionList(optionList, options, connection)) + { + UInt32 bufferSize = optionList.dwSize; + if (!NativeMethods.InternetQueryOptionW( + IntPtr.Zero, + NativeHelpers.INTERNET_OPTION.INTERNET_OPTION_PER_CONNECTION_OPTION, + optionListPtr, + ref bufferSize)) + { + throw new Win32Exception("InternetQueryOptionW(INTERNET_OPTION_PER_CONNECTION_OPTION) failed"); + } + + for (int i = 0; i < options.Length; i++) + { + IntPtr opt = IntPtr.Add(optionList.pOptions, i * Marshal.SizeOf(typeof(NativeHelpers.INTERNET_PER_CONN_OPTIONW))); + NativeHelpers.INTERNET_PER_CONN_OPTIONW option = (NativeHelpers.INTERNET_PER_CONN_OPTIONW)Marshal.PtrToStructure(opt, + typeof(NativeHelpers.INTERNET_PER_CONN_OPTIONW)); + options[i].Value = option.Value; + option.Value = null; // Stops the GC from freeing the same memory twice + } + } + } + + internal static void SetOption(NativeHelpers.INTERNET_PER_CONN_OPTIONW[] options, string connection = null) + { + using (NativeHelpers.INTERNET_PER_CONN_OPTION_LISTW optionList = new NativeHelpers.INTERNET_PER_CONN_OPTION_LISTW()) + using (SafeMemoryBuffer optionListPtr = MarshalOptionList(optionList, options, connection)) + { + if (!NativeMethods.InternetSetOptionW( + IntPtr.Zero, + NativeHelpers.INTERNET_OPTION.INTERNET_OPTION_PER_CONNECTION_OPTION, + optionListPtr, + optionList.dwSize)) + { + throw new Win32Exception("InternetSetOptionW(INTERNET_OPTION_PER_CONNECTION_OPTION) failed"); + } + } + } + + internal static SafeMemoryBuffer MarshalOptionList(NativeHelpers.INTERNET_PER_CONN_OPTION_LISTW optionList, + NativeHelpers.INTERNET_PER_CONN_OPTIONW[] options, string connection) + { + optionList.pszConnection = Marshal.StringToHGlobalUni(connection); + optionList.dwOptionCount = (UInt32)options.Length; + + int optionSize = Marshal.SizeOf(typeof(NativeHelpers.INTERNET_PER_CONN_OPTIONW)); + optionList.pOptions = Marshal.AllocHGlobal(optionSize * options.Length); + for (int i = 0; i < options.Length; i++) + { + IntPtr option = IntPtr.Add(optionList.pOptions, i * optionSize); + Marshal.StructureToPtr(options[i], option, false); + } + + SafeMemoryBuffer optionListPtr = new SafeMemoryBuffer((int)optionList.dwSize); + Marshal.StructureToPtr(optionList, optionListPtr.DangerousGetHandle(), false); + return optionListPtr; + } + } +} +'@ +Add-CSharpType -References $win_inet_invoke -AnsibleModule $module + +# We need to validate the connection because WinINet will just silently continue even if the connection does not +# already exist. +if ($null -ne $connection -and -not [Ansible.WinINetProxy.WinINetProxy]::IsValidConnection($connection)) { + $module.FailJson("The connection '$connection' does not exist.") +} + +$actual_proxy = New-Object -TypeName Ansible.WinINetProxy.WinINetProxy -ArgumentList @(, $connection) +$module.Diff.before = @{ + auto_config_url = $actual_proxy.AutoConfigUrl + auto_detect = $actual_proxy.AutoDetect + bypass = $actual_proxy.ProxyBypass + server = $actual_proxy.Proxy +} + +# Make sure an empty string is converted to $null for easier comparisons +if ([String]::IsNullOrEmpty($auto_config_url)) { + $auto_config_url = $null +} +if ([String]::IsNullOrEmpty($proxy)) { + $proxy = $null +} +if ([String]::IsNullOrEmpty($bypass)) { + $bypass = $null +} + +# Record the original values in case we need to revert on a failure +$previous_auto_config_url = $actual_proxy.AutoConfigUrl +$previous_auto_detect = $actual_proxy.AutoDetect +$previous_proxy = $actual_proxy.Proxy +$previous_bypass = $actual_proxy.ProxyBypass + +$changed = $false +if ($auto_config_url -ne $previous_auto_config_url) { + $actual_proxy.AutoConfigUrl = $auto_config_url + $changed = $true +} + +if ($auto_detect -ne $previous_auto_detect) { + $actual_proxy.AutoDetect = $auto_detect + $changed = $true +} + +if ($proxy -ne $previous_proxy) { + $actual_proxy.Proxy = $proxy + $changed = $true +} + +if ($bypass -ne $previous_bypass) { + $actual_proxy.ProxyBypass = $bypass + $changed = $true +} + +if ($changed -and -not $module.CheckMode) { + $actual_proxy.Set() + + # Validate that the change was made correctly and revert if it wasn't. THe Set() method won't fail on invalid + # values so we need to check again to make sure all was good + $actual_proxy.Refresh() + if ($actual_proxy.AutoConfigUrl -ne $auto_config_url -or + $actual_proxy.AutoDetect -ne $auto_detect -or + $actual_proxy.Proxy -ne $proxy -or + $actual_proxy.ProxyBypass -ne $bypass) { + + $actual_proxy.AutoConfigUrl = $previous_auto_config_url + $actual_proxy.AutoDetect = $previous_auto_detect + $actual_proxy.Proxy = $previous_proxy + $actual_proxy.ProxyBypass = $previous_bypass + $actual_proxy.Set() + + $module.FailJson("Unknown error when trying to set auto_config_url '$auto_config_url', proxy '$proxy', or bypass '$bypass'") + } +} +$module.Result.changed = $changed + +$module.Diff.after = @{ + auto_config_url = $auto_config_url + auto_detect = $auto_detect + bypass = $bypass + proxy = $proxy +} + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_inet_proxy.py b/ansible_collections/community/windows/plugins/modules/win_inet_proxy.py new file mode 100644 index 000000000..2810606ba --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_inet_proxy.py @@ -0,0 +1,171 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +DOCUMENTATION = r''' +--- +module: win_inet_proxy +short_description: Manages proxy settings for WinINet and Internet Explorer +description: +- Used to set or remove proxy settings for Windows INet which includes Internet + Explorer. +- WinINet is a framework used by interactive applications to submit web + requests through. +- The proxy settings can also be used by other applications like Firefox, + Chrome, and others but there is no definitive list. +options: + auto_detect: + description: + - Whether to configure WinINet to automatically detect proxy settings + through Web Proxy Auto-Detection C(WPAD). + - This corresponds to the checkbox I(Automatically detect settings) in the + connection settings window. + default: yes + type: bool + auto_config_url: + description: + - The URL of a proxy configuration script. + - Proxy configuration scripts are typically JavaScript files with the + C(.pac) extension that implement the C(FindProxyForURL(url, host) + function. + - Omit, set to null or an empty string to remove the auto config URL. + - This corresponds to the checkbox I(Use automatic configuration script) in + the connection settings window. + type: str + bypass: + description: + - A list of hosts that will bypass the set proxy when being accessed. + - Use C(<local>) to match hostnames that are not fully qualified domain + names. This is useful when needing to connect to intranet sites using + just the hostname. If defined, this should be the last entry in the + bypass list. + - Use C(<-loopback>) to stop automatically bypassing the proxy when + connecting through any loopback address like C(127.0.0.1), C(localhost), + or the local hostname. + - Omit, set to null or an empty string/list to remove the bypass list. + - If this is set then I(proxy) must also be set. + type: list + elements: str + connection: + description: + - The name of the IE connection to set the proxy settings for. + - These are the connections under the I(Dial-up and Virtual Private Network) + header in the IE settings. + - When omitted, the default LAN connection is used. + type: str + proxy: + description: + - A string or dict that specifies the proxy to be set. + - If setting a string, should be in the form C(hostname), C(hostname:port), + or C(protocol=hostname:port). + - If the port is undefined, the default port for the protocol in use is + used. + - If setting a dict, the keys should be the protocol and the values should + be the hostname and/or port for that protocol. + - Valid protocols are C(http), C(https), C(ftp), and C(socks). + - Omit, set to null or an empty string to remove the proxy settings. + type: raw +notes: +- This is not the same as the proxy settings set in WinHTTP through the + C(netsh) command. Use the M(community.windows.win_http_proxy) module to manage that instead. +- These settings are by default set per user and not system wide. A registry + property must be set independently from this module if you wish to apply the + proxy for all users. See examples for more detail. +- If per user proxy settings are desired, use I(become) to become any local + user on the host. No password is needed to be set for this to work. +- If the proxy requires authentication, set the credentials using the + M(community.windows.win_credential) module. This requires I(become) to be used so the + credential store can be accessed. +seealso: +- module: community.windows.win_http_proxy +- module: community.windows.win_credential +author: +- Jordan Borean (@jborean93) +''' + +EXAMPLES = r''' +# This should be set before running the win_inet_proxy module +- name: Configure IE proxy settings to apply to all users + ansible.windows.win_regedit: + path: HKLM:\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings + name: ProxySettingsPerUser + data: 0 + type: dword + state: present + +# This should be set before running the win_inet_proxy module +- name: Configure IE proxy settings to apply per user + ansible.windows.win_regedit: + path: HKLM:\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings + name: ProxySettingsPerUser + data: 1 + type: dword + state: present + +- name: Configure IE proxy to use auto detected settings without an explicit proxy + win_inet_proxy: + auto_detect: yes + +- name: Configure IE proxy to use auto detected settings with a configuration script + win_inet_proxy: + auto_detect: yes + auto_config_url: http://proxy.ansible.com/proxy.pac + +- name: Configure IE to use explicit proxy host + win_inet_proxy: + auto_detect: yes + proxy: ansible.proxy + +- name: Configure IE to use explicit proxy host with port and without auto detection + win_inet_proxy: + auto_detect: no + proxy: ansible.proxy:8080 + +- name: Configure IE to use a specific proxy per protocol + win_inet_proxy: + proxy: + http: ansible.proxy:8080 + https: ansible.proxy:8443 + +- name: Configure IE to use a specific proxy per protocol using a string + win_inet_proxy: + proxy: http=ansible.proxy:8080;https=ansible.proxy:8443 + +- name: Set a proxy with a bypass list + win_inet_proxy: + proxy: ansible.proxy + bypass: + - server1 + - server2 + - <-loopback> + - <local> + +- name: Remove any explicit proxies that are set + win_inet_proxy: + proxy: '' + bypass: '' + +# This should be done after setting the IE proxy with win_inet_proxy +- name: Import IE proxy configuration to WinHTTP + win_http_proxy: + source: ie + +# Explicit credentials can only be set per user and require become to work +- name: Set credential to use for proxy auth + win_credential: + name: ansible.proxy # The name should be the FQDN of the proxy host + type: generic_password + username: proxyuser + secret: proxypass + state: present + become: yes + become_user: '{{ ansible_user }}' + become_method: runas +''' + +RETURN = r''' +# +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_initialize_disk.ps1 b/ansible_collections/community/windows/plugins/modules/win_initialize_disk.ps1 new file mode 100644 index 000000000..21a6b9804 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_initialize_disk.ps1 @@ -0,0 +1,163 @@ +#!powershell + +# Copyright: (c) 2019, Brant Evans <bevans@redhat.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic +#AnsibleRequires -OSVersion 6.2 + +Set-StrictMode -Version 2 + +$spec = @{ + options = @{ + disk_number = @{ type = "int" } + uniqueid = @{ type = "str" } + path = @{ type = "str" } + style = @{ type = "str"; choices = "gpt", "mbr"; default = "gpt" } + online = @{ type = "bool"; default = $true } + force = @{ type = "bool"; default = $false } + } + mutually_exclusive = @( + , @('disk_number', 'uniqueid', 'path') + ) + required_one_of = @( + , @('disk_number', 'uniqueid', 'path') + ) + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$disk_number = $module.Params.disk_number +$uniqueid = $module.Params.uniqueid +$path = $module.Params.path +$partition_style = $module.Params.style +$bring_online = $module.Params.online +$force_init = $module.Params.force + +function Get-AnsibleDisk { + param( + $DiskNumber, + $UniqueId, + $Path + ) + + if ($null -ne $DiskNumber) { + try { + $disk = Get-Disk -Number $DiskNumber + } + catch { + $module.FailJson("There was an error retrieving the disk using disk_number $($DiskNumber): $($_.Exception.Message)") + } + } + elseif ($null -ne $UniqueId) { + try { + $disk = Get-Disk -UniqueId $UniqueId + } + catch { + $module.FailJson("There was an error retrieving the disk using id $($UniqueId): $($_.Exception.Message)") + } + } + elseif ($null -ne $Path) { + try { + $disk = Get-Disk -Path $Path + } + catch { + $module.FailJson("There was an error retrieving the disk using path $($Path): $($_.Exception.Message)") + } + } + else { + $module.FailJson("Unable to retrieve disk: disk_number, id, or path was not specified") + } + + return $disk +} + +function Initialize-AnsibleDisk { + param( + $AnsibleDisk, + $PartitionStyle + ) + + if ($AnsibleDisk.IsReadOnly) { + $module.FailJson("Unable to initialize disk as it is read-only") + } + + $parameters = @{ + Number = $AnsibleDisk.Number + PartitionStyle = $PartitionStyle + } + + if (-Not $module.CheckMode) { + Initialize-Disk @parameters -Confirm:$false + } + + $module.Result.changed = $true +} + +function Clear-AnsibleDisk { + param( + $AnsibleDisk + ) + + $parameters = @{ + Number = $AnsibleDisk.Number + } + + if (-Not $module.CheckMode) { + Clear-Disk @parameters -RemoveData -RemoveOEM -Confirm:$false + } +} + +function Set-AnsibleDisk { + param( + $AnsibleDisk, + $BringOnline + ) + + $refresh_disk_status = $false + + if ($BringOnline) { + if (-Not $module.CheckMode) { + if ($AnsibleDisk.IsOffline) { + Set-Disk -Number $AnsibleDisk.Number -IsOffline:$false + $refresh_disk_status = $true + } + + if ($AnsibleDisk.IsReadOnly) { + Set-Disk -Number $AnsibleDisk.Number -IsReadOnly:$false + $refresh_disk_status = $true + } + } + } + + if ($refresh_disk_status) { + $AnsibleDisk = Get-AnsibleDisk -DiskNumber $AnsibleDisk.Number + } + + return $AnsibleDisk +} + +$ansible_disk = Get-AnsibleDisk -DiskNumber $disk_number -UniqueId $uniqueid -Path $path +$ansible_part_style = $ansible_disk.PartitionStyle + +if ("RAW" -eq $ansible_part_style) { + $ansible_disk = Set-AnsibleDisk -AnsibleDisk $ansible_disk -BringOnline $bring_online + Initialize-AnsibleDisk -AnsibleDisk $ansible_disk -PartitionStyle $partition_style +} +else { + if (($ansible_part_style -ne $partition_style.ToUpper()) -And -Not $force_init) { + $msg = -join @( + "Force initialization must be specified since the target partition style: $($partition_style.ToLower()) " + "is different from the current partition style: $($ansible_part_style.ToLower())" + ) + $module.FailJson($msg) + } + elseif ($force_init) { + $ansible_disk = Set-AnsibleDisk -AnsibleDisk $ansible_disk -BringOnline $bring_online + Clear-AnsibleDisk -AnsibleDisk $ansible_disk + if ( $bring_online ) { Initialize-AnsibleDisk -AnsibleDisk $ansible_disk -PartitionStyle $partition_style } + } +} + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_initialize_disk.py b/ansible_collections/community/windows/plugins/modules/win_initialize_disk.py new file mode 100644 index 000000000..485b4a51f --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_initialize_disk.py @@ -0,0 +1,75 @@ +#!/usr/bin/python + +# Copyright: (c) 2019, Brant Evans <bevans@redhat.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = ''' +--- +module: win_initialize_disk +short_description: Initializes disks on Windows Server +description: + - "The M(community.windows.win_initialize_disk) module initializes disks" +options: + disk_number: + description: + - Used to specify the disk number of the disk to be initialized. + type: int + uniqueid: + description: + - Used to specify the uniqueid of the disk to be initialized. + type: str + path: + description: + - Used to specify the path to the disk to be initialized. + type: str + style: + description: + - The partition style to use for the disk. Valid options are mbr or gpt. + type: str + choices: [ gpt, mbr ] + default: gpt + online: + description: + - If the disk is offline and/or readonly update the disk to be online and not readonly. + type: bool + default: true + force: + description: + - Specify if initializing should be forced for disks that are already initialized. + type: bool + default: no + +notes: + - One of three parameters (I(disk_number), I(uniqueid), and I(path)) are mandatory to identify the target disk, but + more than one cannot be specified at the same time. + - A minimum Operating System Version of Server 2012 or Windows 8 is required to use this module. + - This module is idempotent if I(force) is not specified. + +seealso: + - module: community.windows.win_disk_facts + - module: community.windows.win_partition + - module: community.windows.win_format + +author: + - Brant Evans (@branic) +''' + +EXAMPLES = ''' +- name: Initialize a disk + community.windows.win_initialize_disk: + disk_number: 1 + +- name: Initialize a disk with an MBR partition style + community.windows.win_initialize_disk: + disk_number: 1 + style: mbr + +- name: Forcefully initiallize a disk + community.windows.win_initialize_disk: + disk_number: 2 + force: yes +''' + +RETURN = ''' +# +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_lineinfile.ps1 b/ansible_collections/community/windows/plugins/modules/win_lineinfile.ps1 new file mode 100644 index 000000000..25af6782d --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_lineinfile.ps1 @@ -0,0 +1,483 @@ +#!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 -LiteralPath $temppath -force + Fail-Json @{} "failed to validate $cmdname $cmdargs with error: $output $error" + } + + } + + # Commit changes to the path + $cleanpath = $path.Replace("/", "\") + Try { + Copy-Item -LiteralPath $temppath -Destination $cleanpath -Force -WhatIf:$check_mode + } + Catch { + Fail-Json @{} "Cannot write to: $cleanpath ($($_.Exception.Message))" + } + + Try { + Remove-Item -LiteralPath $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 { + param ( + $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("/", "\") + $endswithnewline = $null + + # 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) + $endswithnewline = $false + } + + # Initialize result information + $result = @{ + backup = "" + changed = $false + msg = "" + } + + If ($insertbefore -and $insertafter) { + Add-Warning $result "Both insertbefore and insertafter parameters found, ignoring `"insertafter=$insertafter`"" + } + + # 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 ($null -eq $endswithnewline ) { + $alltext = [System.IO.File]::ReadAllText($cleanpath, $encodingobj) + $endswithnewline = (($alltext[-1] -eq "`n") -or ($alltext[-1] -eq "`r")) + } + } + + if ($diff_support) { + if ($endswithnewline) { + $before += "" + } + $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 + } + + if ($endswithnewline) { + $lines.Add("") + } + + $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 + $alltext = [System.IO.File]::ReadAllText($cleanpath, $encodingobj) + If (($alltext[-1] -eq "`n") -or ($alltext[-1] -eq "`r")) { + $lines.Add("") + $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 (-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/ansible_collections/community/windows/plugins/modules/win_lineinfile.py b/ansible_collections/community/windows/plugins/modules/win_lineinfile.py new file mode 100644 index 000000000..64b5a7d42 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_lineinfile.py @@ -0,0 +1,171 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +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. +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. + 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 +seealso: +- module: ansible.builtin.assemble +- module: ansible.builtin.lineinfile +author: +- Brian Lloyd (@brianlloyd) +''' + +EXAMPLES = r''' +- name: Insert path without converting \r\n + community.windows.win_lineinfile: + path: c:\file.txt + line: c:\return\new + +- community.windows.win_lineinfile: + path: C:\Temp\example.conf + regex: '^name=' + line: 'name=JohnDoe' + +- community.windows.win_lineinfile: + path: C:\Temp\example.conf + regex: '^name=' + state: absent + +- community.windows.win_lineinfile: + path: C:\Temp\example.conf + regex: '^127\.0\.0\.1' + line: '127.0.0.1 localhost' + +- community.windows.win_lineinfile: + path: C:\Temp\httpd.conf + regex: '^Listen ' + insertafter: '^#Listen ' + line: Listen 8080 + +- community.windows.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 + community.windows.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 + community.windows.win_lineinfile: + path: C:\Temp\testfile.txt + line: Line added to file + newline: unix + +- name: Update a line using backrefs + community.windows.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/ansible_collections/community/windows/plugins/modules/win_listen_ports_facts.ps1 b/ansible_collections/community/windows/plugins/modules/win_listen_ports_facts.ps1 new file mode 100644 index 000000000..4ad3d68d4 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_listen_ports_facts.ps1 @@ -0,0 +1,90 @@ +#!powershell + +# Copyright: (c) 2022, DataDope (@datadope-io) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic + +$spec = @{ + options = @{ + date_format = @{ type = 'str'; default = '%c' } + tcp_filter = @{ type = 'list'; elements = 'str'; default = 'Listen' } + } + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$date_format = $module.Params.date_format +$tcp_filter = $module.Params.tcp_filter + +# Structure of the response the script will return +$ansibleFacts = @{ + tcp_listen = @() + udp_listen = @() +} + +# Build an index of the processes based on the PID +$processes = @{} +Get-CimInstance -ClassName Win32_Process | ForEach-Object { + $processes[[int]$_.ProcessId] = $_ +} + + +# Format the given date with the same format as listen_port_facts stime (Date and time - abbreviated) by default, or +# with the given format +function Format-Date { + param ( + $date + ) + + if ($null -ne $date) { + $date = Get-Date $date -UFormat $date_format + } + + return $date +} + +# Return the processed listener and the associated PID data +function Build-Listener { + param ( + $listener, + $type + ) + + $process = $processes[[int]$listener.OwningProcess] + $process_owner = Invoke-CimMethod -InputObject $process -MethodName GetOwner + + $owner = $null + if ($null -ne $process_owner.User -and $null -ne $process_owner.Domain) { + $owner = $process_owner.Domain + '\' + $process_owner.User + } + + return @{ + address = $listener.LocalAddress + name = $process.Name + pid = $listener.OwningProcess + port = $listener.LocalPort + protocol = $type + stime = Format-Date $process.CreationDate + user = $owner + } +} + +try { + # Retrieve the information of the TCP ports with Listen status by default, or with the given state/s + Get-NetTCPConnection -State $tcp_filter -ErrorAction SilentlyContinue | Foreach-Object { + $ansibleFacts.tcp_listen += Build-Listener $_ "tcp" + } + + # Retrieve the information of the UDP ports + Get-NetUDPEndpoint | Foreach-Object { + $ansibleFacts.udp_listen += Build-Listener $_ "udp" + } +} +catch { + $module.FailJson("An error occurred while retrieving ports facts: $($_.Exception.Message)", $_) +} + +$module.Result.ansible_facts = $ansibleFacts +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_listen_ports_facts.py b/ansible_collections/community/windows/plugins/modules/win_listen_ports_facts.py new file mode 100644 index 000000000..89d51be87 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_listen_ports_facts.py @@ -0,0 +1,95 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2022, DataDope (@datadope-io) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_listen_ports_facts +version_added: '1.10.0' +short_description: Recopilates the facts of the listening ports of the machine +description: + - Recopilates the information of the TCP and UDP ports of the machine and + the related processes. + - State of the TCP ports could be filtered, as well as the format of the + date when the parent process was launched. + - The module's goal is to replicate the functionality of the linux module + listen_ports_facts, mantaining the format of the said module. +options: + date_format: + description: + - The format of the date when the process that owns the port started. + - The date specification is UFormat + type: str + default: '%c' + tcp_filter: + description: + - Filter for the state of the TCP ports that will be recopilated. + - Supports multiple states (Bound, Closed, CloseWait, Closing, DeleteTCB, + Established, FinWait1, FinWait2, LastAck, Listen, SynReceived, SynSent + and TimeWait), that can be used alone or combined. Note that the Bound + state is only available on PowerShell version 4.0 or later. + type: list + elements: str + default: [ Listen ] +notes: +- The generated data (tcp_listen and udp_listen) and the fields within follows + the listen_ports_facts schema to achieve compatibility with the said module + output, even though this module if capable of extracting ports with a state + other than Listen +seealso: +- module: community.general.listen_ports_facts +author: +- David Nieto (@david-ns) +''' + +EXAMPLES = r''' +- name: Recopilate ports facts + community.windows.win_listen_ports_facts: + +- name: Retrieve only ports with Closing and Established states + community.windows.win_listen_ports_facts: + tcp_filter: + - Closing + - Established + +- name: Get ports facts with only the year within the date field + community.windows.win_listen_ports_facts: + date_format: '%Y' +''' + +RETURN = r''' +tcp_listen: + description: List of dicts with the detected TCP ports + returned: success + type: list + elements: dict + sample: [ + { + "address": "127.0.0.1", + "name": "python", + "pid": 5332, + "port": 82, + "protocol": "tcp", + "stime": "Thu Nov 18 15:27:42 2021", + "user": "SERVER\\Administrator" + } + ] +udp_listen: + description: List of dicts with the detected UDP ports + returned: success + type: list + elements: dict + sample: [ + { + "address": "127.0.0.1", + "name": "python", + "pid": 5332, + "port": 82, + "protocol": "udp", + "stime": "Thu Nov 18 15:27:42 2021", + "user": "SERVER\\Administrator" + } + ] +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_mapped_drive.ps1 b/ansible_collections/community/windows/plugins/modules/win_mapped_drive.ps1 new file mode 100644 index 000000000..210ae223a --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_mapped_drive.ps1 @@ -0,0 +1,449 @@ +#!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.AccessToken +#AnsibleRequires -CSharpUtil Ansible.Basic +#Requires -Module Ansible.ModuleUtils.AddType + +$spec = @{ + options = @{ + letter = @{ type = "str"; required = $true } + path = @{ type = "path"; } + state = @{ type = "str"; default = "present"; choices = @("absent", "present") } + username = @{ type = "str" } + password = @{ type = "str"; no_log = $true } + } + required_if = @( + , @("state", "present", @("path")) + ) + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$letter = $module.Params.letter +$path = $module.Params.path +$state = $module.Params.state +$username = $module.Params.username +$password = $module.Params.password + +if ($letter -notmatch "^[a-zA-z]{1}$") { + $module.FailJson("letter must be a single letter from A-Z, was: $letter") +} +$letter_root = "$($letter):" + +$module.Diff.before = "" +$module.Diff.after = "" + +Add-CSharpType -AnsibleModule $module -References @' +using Microsoft.Win32.SafeHandles; +using System; +using System.Collections.Generic; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; + +namespace Ansible.MappedDrive +{ + internal class NativeHelpers + { + public enum ResourceScope : uint + { + Connected = 0x00000001, + GlobalNet = 0x00000002, + Remembered = 0x00000003, + Recent = 0x00000004, + Context = 0x00000005, + } + + [Flags] + public enum ResourceType : uint + { + Any = 0x0000000, + Disk = 0x00000001, + Print = 0x00000002, + Reserved = 0x00000008, + Unknown = 0xFFFFFFFF, + } + + public enum CloseFlags : uint + { + None = 0x00000000, + UpdateProfile = 0x00000001, + } + + [Flags] + public enum AddFlags : uint + { + UpdateProfile = 0x00000001, + UpdateRecent = 0x00000002, + Temporary = 0x00000004, + Interactive = 0x00000008, + Prompt = 0x00000010, + Redirect = 0x00000080, + CurrentMedia = 0x00000200, + CommandLine = 0x00000800, + CmdSaveCred = 0x00001000, + CredReset = 0x00002000, + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct NETRESOURCEW + { + public ResourceScope dwScope; + public ResourceType dwType; + public UInt32 dwDisplayType; + public UInt32 dwUsage; + [MarshalAs(UnmanagedType.LPWStr)] public string lpLocalName; + [MarshalAs(UnmanagedType.LPWStr)] public string lpRemoteName; + [MarshalAs(UnmanagedType.LPWStr)] public string lpComment; + [MarshalAs(UnmanagedType.LPWStr)] public string lpProvider; + } + } + + internal class NativeMethods + { + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool CloseHandle( + IntPtr hObject); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool ImpersonateLoggedOnUser( + IntPtr hToken); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool RevertToSelf(); + + [DllImport("Mpr.dll", CharSet = CharSet.Unicode)] + public static extern UInt32 WNetAddConnection2W( + NativeHelpers.NETRESOURCEW lpNetResource, + [MarshalAs(UnmanagedType.LPWStr)] string lpPassword, + [MarshalAs(UnmanagedType.LPWStr)] string lpUserName, + NativeHelpers.AddFlags dwFlags); + + [DllImport("Mpr.dll", CharSet = CharSet.Unicode)] + public static extern UInt32 WNetCancelConnection2W( + [MarshalAs(UnmanagedType.LPWStr)] string lpName, + NativeHelpers.CloseFlags dwFlags, + bool fForce); + + [DllImport("Mpr.dll")] + public static extern UInt32 WNetCloseEnum( + IntPtr hEnum); + + [DllImport("Mpr.dll", CharSet = CharSet.Unicode)] + public static extern UInt32 WNetEnumResourceW( + IntPtr hEnum, + ref Int32 lpcCount, + SafeMemoryBuffer lpBuffer, + ref UInt32 lpBufferSize); + + [DllImport("Mpr.dll", CharSet = CharSet.Unicode)] + public static extern UInt32 WNetOpenEnumW( + NativeHelpers.ResourceScope dwScope, + NativeHelpers.ResourceType dwType, + UInt32 dwUsage, + IntPtr lpNetResource, + out IntPtr lphEnum); + } + + internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeMemoryBuffer() : base(true) { } + public SafeMemoryBuffer(int cb) : base(true) + { + 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 Impersonation : IDisposable + { + private IntPtr hToken = IntPtr.Zero; + + public Impersonation(IntPtr token) + { + hToken = token; + if (token != IntPtr.Zero) + if (!NativeMethods.ImpersonateLoggedOnUser(hToken)) + throw new Win32Exception("Failed to impersonate token with ImpersonateLoggedOnUser()"); + } + + public void Dispose() + { + if (hToken != null) + NativeMethods.RevertToSelf(); + GC.SuppressFinalize(this); + } + ~Impersonation() { Dispose(); } + } + + public class DriveInfo + { + public string Drive; + public string Path; + } + + 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 Utils + { + private const UInt32 ERROR_SUCCESS = 0x00000000; + private const UInt32 ERROR_NO_MORE_ITEMS = 0x0000103; + + public static void AddMappedDrive(string drive, string path, IntPtr iToken, string username = null, string password = null) + { + NativeHelpers.NETRESOURCEW resource = new NativeHelpers.NETRESOURCEW + { + dwType = NativeHelpers.ResourceType.Disk, + lpLocalName = drive, + lpRemoteName = path, + }; + NativeHelpers.AddFlags dwFlags = NativeHelpers.AddFlags.UpdateProfile; + // While WNetAddConnection2W supports user/pass, this is only used for the first connection and the + // password is not remembered. We will delete the username mapping afterwards as it interferes with + // the implicit credential cache used in Windows + using (Impersonation imp = new Impersonation(iToken)) + { + UInt32 res = NativeMethods.WNetAddConnection2W(resource, password, username, dwFlags); + if (res != ERROR_SUCCESS) + throw new Win32Exception((int)res, String.Format("Failed to map {0} to '{1}' with WNetAddConnection2W()", drive, path)); + } + } + + public static List<DriveInfo> GetMappedDrives(IntPtr iToken) + { + using (Impersonation imp = new Impersonation(iToken)) + { + IntPtr enumPtr = IntPtr.Zero; + UInt32 res = NativeMethods.WNetOpenEnumW(NativeHelpers.ResourceScope.Remembered, NativeHelpers.ResourceType.Disk, + 0, IntPtr.Zero, out enumPtr); + if (res != ERROR_SUCCESS) + throw new Win32Exception((int)res, "WNetOpenEnumW()"); + + List<DriveInfo> resources = new List<DriveInfo>(); + try + { + // MS recommend a buffer size of 16 KiB + UInt32 bufferSize = 16384; + int lpcCount = -1; + + // keep iterating the enum until ERROR_NO_MORE_ITEMS is returned + do + { + using (SafeMemoryBuffer buffer = new SafeMemoryBuffer((int)bufferSize)) + { + res = NativeMethods.WNetEnumResourceW(enumPtr, ref lpcCount, buffer, ref bufferSize); + if (res == ERROR_NO_MORE_ITEMS) + continue; + else if (res != ERROR_SUCCESS) + throw new Win32Exception((int)res, "WNetEnumResourceW()"); + lpcCount = lpcCount < 0 ? 0 : lpcCount; + + NativeHelpers.NETRESOURCEW[] rawResources = new NativeHelpers.NETRESOURCEW[lpcCount]; + PtrToStructureArray(rawResources, buffer.DangerousGetHandle()); + foreach (NativeHelpers.NETRESOURCEW resource in rawResources) + { + DriveInfo currentDrive = new DriveInfo + { + Drive = resource.lpLocalName, + Path = resource.lpRemoteName, + }; + resources.Add(currentDrive); + } + } + } + while (res != ERROR_NO_MORE_ITEMS); + } + finally + { + NativeMethods.WNetCloseEnum(enumPtr); + } + + return resources; + } + } + + public static void RemoveMappedDrive(string drive, IntPtr iToken) + { + using (Impersonation imp = new Impersonation(iToken)) + { + UInt32 res = NativeMethods.WNetCancelConnection2W(drive, NativeHelpers.CloseFlags.UpdateProfile, true); + if (res != ERROR_SUCCESS) + throw new Win32Exception((int)res, String.Format("Failed to remove mapped drive {0} with WNetCancelConnection2W()", drive)); + } + } + + private static void PtrToStructureArray<T>(T[] array, IntPtr ptr) + { + IntPtr ptrOffset = ptr; + for (int i = 0; i < array.Length; i++, ptrOffset = IntPtr.Add(ptrOffset, Marshal.SizeOf(typeof(T)))) + array[i] = (T)Marshal.PtrToStructure(ptrOffset, typeof(T)); + } + } +} +'@ + +Function Get-LimitedToken { + $h_process = [Ansible.AccessToken.TokenUtil]::OpenProcess() + $h_token = [Ansible.AccessToken.TokenUtil]::OpenProcessToken($h_process, "Duplicate, Query") + + try { + # If we don't have a Full token, we don't need to get the limited one to set a mapped drive + $tet = [Ansible.AccessToken.TokenUtil]::GetTokenElevationType($h_token) + if ($tet -ne [Ansible.AccessToken.TokenElevationType]::Full) { + return + } + + foreach ($system_token in [Ansible.AccessToken.TokenUtil]::EnumerateUserTokens("S-1-5-18", "Duplicate")) { + # To get the TokenLinkedToken we need the SeTcbPrivilege, not all SYSTEM tokens have this assigned so + # we need to check before impersonating that token + $token_privileges = [Ansible.AccessToken.TokenUtil]::GetTokenPrivileges($system_token) + if ($null -eq ($token_privileges | Where-Object { $_.Name -eq "SeTcbPrivilege" })) { + continue + } + + [Ansible.AccessToken.TokenUtil]::ImpersonateToken($system_token) + try { + return [Ansible.AccessToken.TokenUtil]::GetTokenLinkedToken($h_token) + } + finally { + [Ansible.AccessToken.TokenUtil]::RevertToSelf() + } + } + } + finally { + $h_token.Dispose() + } +} + +<# +When we run with become and UAC is enabled, the become process will most likely be the Admin/Full token. This is +an issue with the WNetConnection APIs as the Full token is unable to add/enumerate/remove connections due to +Windows storing the connection details on each token session ID. Unless EnabledLinkedConnections (reg key) is +set to 1, the Full token is unable to manage connections in a persisted way whereas the Limited token is. This +is similar to running 'net use' normally and an admin process is unable to see those and vice versa. + +To overcome this problem, we attempt to get a handle on the Limited token for the current logon and impersonate +that before making any WNetConnection calls. If the token is not split, or we are already running on the Limited +token then no impersonatoin is used/required. This allows the module to run with become (required to access the +credential store) but still be able to manage the mapped connections. + +These are the following scenarios we have to handle; + + 1. Run without become + A network logon is usually not split so GetLimitedToken() will return $null and no impersonation is needed + 2. Run with become on admin user with admin priv + We will have a Full token, GetLimitedToken() will return the limited token and impersonation is used + 3. Run with become on admin user without admin priv + We are already running with a Limited token, GetLimitedToken() return $nul and no impersonation is needed + 4. Run with become on standard user + There's no split token, GetLimitedToken() will return $null and no impersonation is needed +#> +$impersonation_token = Get-LimitedToken + +try { + $i_token_ptr = [System.IntPtr]::Zero + if ($null -ne $impersonation_token) { + $i_token_ptr = $impersonation_token.DangerousGetHandle() + } + + $existing_targets = [Ansible.MappedDrive.Utils]::GetMappedDrives($i_token_ptr) + $existing_target = $existing_targets | Where-Object { $_.Drive -eq $letter_root } + + if ($existing_target) { + $module.Diff.before = @{ + letter = $letter + path = $existing_target.Path + } + } + + if ($state -eq "absent") { + if ($null -ne $existing_target) { + if ($null -ne $path -and $existing_target.Path -ne $path) { + $module.FailJson("did not delete mapped drive $letter, the target path is pointing to a different location at $( $existing_target.Path )") + } + if (-not $module.CheckMode) { + [Ansible.MappedDrive.Utils]::RemoveMappedDrive($letter_root, $i_token_ptr) + } + + $module.Result.changed = $true + } + } + else { + $physical_drives = Get-PSDrive -PSProvider "FileSystem" + if ($letter -in $physical_drives.Name) { + $module.FailJson("failed to create mapped drive $letter, this letter is in use and is pointing to a non UNC path") + } + + # PowerShell converts a $null value to "" when crossing the .NET marshaler, we need to convert the input + # to a missing value so it uses the defaults. We also need to Invoke it with MethodInfo.Invoke so the defaults + # are still used + $input_username = $username + if ($null -eq $username) { + $input_username = [Type]::Missing + } + $input_password = $password + if ($null -eq $password) { + $input_password = [Type]::Missing + } + $add_method = [Ansible.MappedDrive.Utils].GetMethod("AddMappedDrive") + + if ($null -ne $existing_target) { + if ($existing_target.Path -ne $path) { + if (-not $module.CheckMode) { + [Ansible.MappedDrive.Utils]::RemoveMappedDrive($letter_root, $i_token_ptr) + $add_method.Invoke($null, [Object[]]@($letter_root, $path, $i_token_ptr, $input_username, $input_password)) + } + $module.Result.changed = $true + } + } + else { + if (-not $module.CheckMode) { + $add_method.Invoke($null, [Object[]]@($letter_root, $path, $i_token_ptr, $input_username, $input_password)) + } + + $module.Result.changed = $true + } + + # If username was set and we made a change, remove the UserName value so Windows will continue to use the cred + # cache. If we don't do this then the drive will fail to map in the future as WNetAddConnection does not cache + # the password and relies on the credential store. + if ($null -ne $username -and $module.Result.changed -and -not $module.CheckMode) { + Set-ItemProperty -LiteralPath HKCU:\Network\$letter -Name UserName -Value "" -WhatIf:$module.CheckMode + } + + $module.Diff.after = @{ + letter = $letter + path = $path + } + } +} +finally { + if ($null -ne $impersonation_token) { + $impersonation_token.Dispose() + } +} + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_mapped_drive.py b/ansible_collections/community/windows/plugins/modules/win_mapped_drive.py new file mode 100644 index 000000000..2762e2b2b --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_mapped_drive.py @@ -0,0 +1,147 @@ +#!/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) + +DOCUMENTATION = r''' +--- +module: win_mapped_drive +short_description: Map network drives for users +description: +- Allows you to modify mapped network drives for individual users. +- Also support WebDAV endpoints in the UNC form. +options: + letter: + description: + - The letter of the network path to map to. + - This letter must not already be in use with Windows. + type: str + required: yes + password: + description: + - The password for C(username) that is used when testing the initial + connection. + - This is never saved with a mapped drive, use the M(community.windows.win_credential) module + to persist a username and password for a host. + type: str + path: + description: + - The UNC path to map the drive to. + - If pointing to a WebDAV location this must still be in a UNC path in the + format C(\\hostname\path) and not a URL, see examples for more details. + - To specify a C(https) WebDAV path, add C(@SSL) after the hostname. To + specify a custom WebDAV port add C(@<port num>) after the C(@SSL) or + hostname portion of the UNC path, e.g. C(\\server@SSL@1234) or + C(\\server@1234). + - This is required if C(state=present). + - If C(state=absent) and I(path) is not set, the module will delete the + mapped drive regardless of the target. + - If C(state=absent) and the I(path) is set, the module will throw an error + if path does not match the target of the mapped drive. + type: path + state: + description: + - If C(present) will ensure the mapped drive exists. + - If C(absent) will ensure the mapped drive does not exist. + type: str + choices: [ absent, present ] + default: present + username: + description: + - The username that is used when testing the initial connection. + - This is never saved with a mapped drive, the M(community.windows.win_credential) module + to persist a username and password for a host. + - This is required if the mapped drive requires authentication with + custom credentials and become, or CredSSP cannot be used. + - If become or CredSSP is used, any credentials saved with + M(community.windows.win_credential) will automatically be used instead. + type: str +notes: +- You cannot use this module to access a mapped drive in another Ansible task, + drives mapped with this module are only accessible when logging in + interactively with the user through the console or RDP. +- It is recommend to run this module with become or CredSSP when the remote + path requires authentication. +- When using become or CredSSP, the task will have access to any local + credentials stored in the user's vault. +- If become or CredSSP is not available, the I(username) and I(password) + options can be used for the initial authentication but these are not + persisted. +- WebDAV paths must have the WebDAV client feature installed for this module to + map those paths. This is installed by default on desktop Windows editions but + Windows Server hosts need to install the C(WebDAV-Redirector) feature using + M(ansible.windows.win_feature). +seealso: +- module: community.windows.win_credential +author: +- Jordan Borean (@jborean93) +''' + +EXAMPLES = r''' +- name: Create a mapped drive under Z + community.windows.win_mapped_drive: + letter: Z + path: \\domain\appdata\accounting + +- name: Delete any mapped drives under Z + community.windows.win_mapped_drive: + letter: Z + state: absent + +- name: Only delete the mapped drive Z if the paths match (error is thrown otherwise) + community.windows.win_mapped_drive: + letter: Z + path: \\domain\appdata\accounting + state: absent + +- name: Create mapped drive with credentials and save the username and password + block: + - name: Save the network credentials required for the mapped drive + community.windows.win_credential: + name: server + type: domain_password + username: username@DOMAIN + secret: Password01 + state: present + + - name: Create a mapped drive that requires authentication + community.windows.win_mapped_drive: + letter: M + path: \\SERVER\C$ + state: present + vars: + # become is required to save and retrieve the credentials in the tasks + ansible_become: yes + ansible_become_method: runas + ansible_become_user: '{{ ansible_user }}' + ansible_become_pass: '{{ ansible_password }}' + +- name: Create mapped drive with credentials that do not persist on the next logon + community.windows.win_mapped_drive: + letter: M + path: \\SERVER\C$ + state: present + username: '{{ ansible_user }}' + password: '{{ ansible_password }}' + +# This should only be required for Windows Server OS' +- name: Ensure WebDAV client feature is installed + ansible.windows.win_feature: + name: WebDAV-Redirector + state: present + register: webdav_feature + +- name: Reboot after installing WebDAV client feature + ansible.windows.win_reboot: + when: webdav_feature.reboot_required + +- name: Map the HTTPS WebDAV location + community.windows.win_mapped_drive: + letter: W + path: \\live.sysinternals.com@SSL\tools # https://live.sysinternals.com/tools + state: present +''' + +RETURN = r''' +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_msg.ps1 b/ansible_collections/community/windows/plugins/modules/win_msg.ps1 new file mode 100644 index 000000000..91763886b --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_msg.ps1 @@ -0,0 +1,52 @@ +#!powershell + +# Copyright: (c) 2016, Jon Hawkesworth (@jhawkesworth) <jhawkesworth@protonmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#Requires -Module Ansible.ModuleUtils.Legacy +# +$stopwatch = [system.diagnostics.stopwatch]::startNew() + +$params = Parse-Args $args -supports_check_mode $true +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false + +$display_seconds = Get-AnsibleParam -obj $params -name "display_seconds" -type "int" -default "10" +$msg = Get-AnsibleParam -obj $params -name "msg" -type "str" -default "Hello world!" +$to = Get-AnsibleParam -obj $params -name "to" -type "str" -default "*" +$wait = Get-AnsibleParam -obj $params -name "wait" -type "bool" -default $false + +$result = @{ + changed = $false + display_seconds = $display_seconds + msg = $msg + wait = $wait +} + +if ($msg.Length -gt 255) { + Fail-Json -obj $result -message "msg length must be less than 256 characters, current length: $($msg.Length)" +} + +$msg_args = @($to, "/TIME:$display_seconds") + +if ($wait) { + $msg_args += "/W" +} + +$msg_args += $msg +if (-not $check_mode) { + $output = & msg.exe $msg_args 2>&1 + $result.rc = $LASTEXITCODE +} + +$endsend_at = Get-Date | Out-String +$stopwatch.Stop() + +$result.changed = $true +$result.runtime_seconds = $stopwatch.Elapsed.TotalSeconds +$result.sent_localtime = $endsend_at.Trim() + +if ($result.rc -ne 0 ) { + Fail-Json -obj $result -message "$output" +} + +Exit-Json $result diff --git a/ansible_collections/community/windows/plugins/modules/win_msg.py b/ansible_collections/community/windows/plugins/modules/win_msg.py new file mode 100644 index 000000000..88a8beb14 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_msg.py @@ -0,0 +1,88 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2017, Jon Hawkesworth (@jhawkesworth) <jhawkesworth@protonmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_msg +short_description: Sends a message to logged in users on Windows hosts +description: + - Wraps the msg.exe command in order to send messages to Windows hosts. +options: + to: + description: + - Who to send the message to. Can be a username, sessionname or sessionid. + type: str + default: '*' + display_seconds: + description: + - How long to wait for receiver to acknowledge message, in seconds. + type: int + default: 10 + wait: + description: + - Whether to wait for users to respond. Module will only wait for the number of seconds specified in display_seconds or 10 seconds if not specified. + However, if I(wait) is C(yes), the message is sent to each logged on user in turn, waiting for the user to either press 'ok' or for + the timeout to elapse before moving on to the next user. + type: bool + default: 'no' + msg: + description: + - The text of the message to be displayed. + - The message must be less than 256 characters. + type: str + default: Hello world! +notes: + - This module must run on a windows host, so ensure your play targets windows + hosts, or delegates to a windows host. + - Messages are only sent to the local host where the module is run. + - The module does not support sending to users listed in a file. + - Setting wait to C(yes) can result in long run times on systems with many logged in users. +seealso: +- module: community.windows.win_say +- module: community.windows.win_toast +author: +- Jon Hawkesworth (@jhawkesworth) +''' + +EXAMPLES = r''' +- name: Warn logged in users of impending upgrade + community.windows.win_msg: + display_seconds: 60 + msg: Automated upgrade about to start. Please save your work and log off before {{ deployment_start_time }} +''' + +RETURN = r''' +msg: + description: Test of the message that was sent. + returned: changed + type: str + sample: Automated upgrade about to start. Please save your work and log off before 22 July 2016 18:00:00 +display_seconds: + description: Value of display_seconds module parameter. + returned: success + type: str + sample: 10 +rc: + description: The return code of the API call. + returned: always + type: int + sample: 0 +runtime_seconds: + description: How long the module took to run on the remote windows host. + returned: success + type: str + sample: 22 July 2016 17:45:51 +sent_localtime: + description: local time from windows host when the message was sent. + returned: success + type: str + sample: 22 July 2016 17:45:51 +wait: + description: Value of wait module parameter. + returned: success + type: bool + sample: false +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_net_adapter_feature.ps1 b/ansible_collections/community/windows/plugins/modules/win_net_adapter_feature.ps1 new file mode 100644 index 000000000..a7377e168 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_net_adapter_feature.ps1 @@ -0,0 +1,67 @@ +#!powershell + +# Copyright: (c) 2020, ライトウェルの人 <jiro.higuchi@shi-g.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic + +$spec = @{ + options = @{ + interface = @{ type = 'list'; elements = 'str'; required = $true } + state = @{ type = 'str'; choices = 'disabled', 'enabled'; default = 'enabled' } + component_id = @{ type = 'list'; elements = 'str'; required = $true } + } + supports_check_mode = $true +} +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$interface = $module.Params.interface +$state = $module.Params.state +$component_id = $module.Params.component_id +$check_mode = $module.CheckMode + +If ($interface -eq "*") { + $interface = Get-NetAdapter | Select-Object -ExpandProperty Name +} +Else { + ForEach ($Interface_name in $interface) { + If (@(Get-NetAdapter | Where-Object Name -eq $Interface_name).Count -eq 0) { + $module.FailJson("Invalid network adapter name: $Interface_name") + } + } +} + +$state = $state -eq "enabled" + +ForEach ($componentID_name in $component_id) { + If (@(Get-NetAdapterBinding | Where-Object ComponentID -eq $componentID_name).Count -eq 0) { + $module.FailJson("Invalid componentID: $componentID_name") + } +} + +$module.Result.changed = $false + +ForEach ($componentID_name in $component_id) { + ForEach ($Interface_name in $interface) { + $current_state = (Get-NetAdapterBinding | where-object { $_.Name -eq $Interface_name } | where-object { $_.ComponentID -eq $componentID_name }).Enabled + #Initialize the check_Idempotency flag for each interface, and for each component_id. + $check_Idempotency = $true + + If ($current_state -eq $state) { + $check_Idempotency = $false + } + + #Even Once $check_Idempotency remains $true, $module.Result.changed turns $true. + $module.Result.changed = $module.Result.changed -Or $check_Idempotency + + If ($check_Idempotency) { + If ($state -eq "True") { + Enable-NetAdapterBinding -Name $Interface_name -ComponentID $componentID_name -WhatIf:$check_mode + } + Else { + Disable-NetAdapterBinding -Name $Interface_name -ComponentID $componentID_name -WhatIf:$check_mode + } + } + } +} +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_net_adapter_feature.py b/ansible_collections/community/windows/plugins/modules/win_net_adapter_feature.py new file mode 100644 index 000000000..91bb40fe5 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_net_adapter_feature.py @@ -0,0 +1,69 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, ライトウェルの人 <jiro.higuchi@shi-g.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +DOCUMENTATION = r''' +--- +module: win_net_adapter_feature +version_added: 1.2.0 +short_description: Enable or disable certain network adapters. +description: + - Enable or disable some network components of a certain network adapter or all the network adapters. +options: + interface: + description: + - Name of Network Adapter Interface. For example, C(Ethernet0) or C(*). + type: list + elements: str + required: yes + state: + description: + - Specify the state of ms_tcpip6 of interfaces. + type: str + choices: [ enabled, disabled ] + default: enabled + required: no + component_id: + description: + - Specify the below component_id of network adapters. + - component_id (DisplayName) + - C(ms_implat) (Microsoft Network Adapter Multiplexor Protocol) + - C(ms_lltdio) (Link-Layer Topology Discovery Mapper I/O Driver) + - C(ms_tcpip6) (Internet Protocol Version 6 (TCP/IPv6)) + - C(ms_tcpip) (Internet Protocol Version 4 (TCP/IPv4)) + - C(ms_lldp) (Microsoft LLDP Protocol Driver) + - C(ms_rspndr) (Link-Layer Topology Discovery Responder) + - C(ms_msclient) (Client for Microsoft Networks) + - C(ms_pacer) (QoS Packet Scheduler) + - If you'd like to set custom adapters like 'Juniper Network Service', get the I(component_id) by running the C(Get-NetAdapterBinding) cmdlet. + type: list + elements: str + required: yes + +author: + - ライトウェルの人 (@jirolin) +''' + + +EXAMPLES = r''' +- name: enable multiple interfaces of multiple interfaces + community.windows.win_net_adapter_feature: + interface: + - 'Ethernet0' + - 'Ethernet1' + state: enabled + component_id: + - ms_tcpip6 + - ms_server + +- name: Enable ms_tcpip6 of all the Interface + community.windows.win_net_adapter_feature: + interface: '*' + state: enabled + component_id: + - ms_tcpip6 + +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_netbios.ps1 b/ansible_collections/community/windows/plugins/modules/win_netbios.ps1 new file mode 100644 index 000000000..5995e580e --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_netbios.ps1 @@ -0,0 +1,70 @@ +#!powershell + +# Copyright: (c) 2019, Thomas Moore (@tmmruk) <hi@tmmr.uk> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic + +$spec = @{ + options = @{ + state = @{ type = "str"; choices = "enabled", "disabled", "default"; required = $true } + adapter_names = @{ type = "list"; elements = "str"; required = $false } + } + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) +$module.Result.reboot_required = $false + +$state = $module.Params.state +$adapter_names = $module.Params.adapter_names + +switch ( $state ) { + 'default' { $netbiosoption = 0 } + enabled { $netbiosoption = 1 } + disabled { $netbiosoption = 2 } +} + +if (-not $adapter_names) { + # Target all network adapters on the system + $get_params = @{ + ClassName = 'Win32_NetworkAdapterConfiguration' + Filter = 'IPEnabled=true' + Property = @('MacAddress', 'TcpipNetbiosOptions') + } + $target_adapters_config = Get-CimInstance @get_params +} +else { + $get_params = @{ + Class = 'Win32_NetworkAdapter' + Filter = ($adapter_names | ForEach-Object -Process { "NetConnectionId='$_'" }) -join " OR " + KeyOnly = $true + } + $target_adapters_config = Get-CimInstance @get_params | Get-CimAssociatedInstance -ResultClass 'Win32_NetworkAdapterConfiguration' + if (($target_adapters_config | Measure-Object).Count -ne $adapter_names.Count) { + $module.FailJson("Not all of the target adapter names could be found on the system. No configuration changes have been made. $adapter_names") + } +} + +foreach ($adapter in $target_adapters_config) { + if ($adapter.TcpipNetbiosOptions -ne $netbiosoption) { + if (-not $module.CheckMode) { + $result = Invoke-CimMethod -InputObject $adapter -MethodName SetTcpipNetbios -Arguments @{TcpipNetbiosOptions = $netbiosoption } + switch ( $result.ReturnValue ) { + 0 { <# Success no reboot required #> } + 1 { $module.Result.reboot_required = $true } + 100 { + $msg = "DHCP not enabled on adapter $($adapter.MacAddress). Unable to set default. Try using disabled or enabled options instead." + $module.Warn($msg) + } + default { + $msg = "An error occurred while setting TcpipNetbios options on adapter $($adapter.MacAddress). Return code $($result.ReturnValue)." + $module.FailJson($msg) + } + } + } + $module.Result.changed = $true + } +} + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_netbios.py b/ansible_collections/community/windows/plugins/modules/win_netbios.py new file mode 100644 index 000000000..80e0fbece --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_netbios.py @@ -0,0 +1,69 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Thomas Moore (@tmmruk) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_netbios +short_description: Manage NetBIOS over TCP/IP settings on Windows. +description: + - Enables or disables NetBIOS on Windows network adapters. + - Can be used to protect a system against NBT-NS poisoning and avoid NBNS broadcast storms. + - Settings can be applied system wide or per adapter. +options: + state: + description: + - Whether NetBIOS should be enabled, disabled, or default (use setting from DHCP server or if static IP address is assigned enable NetBIOS). + choices: + - enabled + - disabled + - default + required: yes + type: str + adapter_names: + description: + - List of adapter names for which to manage NetBIOS settings. If this option is omitted then configuration is applied to all adapters on the system. + - The adapter name used is the connection caption in the Network Control Panel or via C(Get-NetAdapter), eg C(Ethernet 2). + type: list + elements: str + required: no + +author: + - Thomas Moore (@tmmruk) +notes: + - Changing NetBIOS settings does not usually require a reboot and will take effect immediately. + - UDP port 137/138/139 will no longer be listening once NetBIOS is disabled. +''' + +EXAMPLES = r''' +- name: Disable NetBIOS system wide + community.windows.win_netbios: + state: disabled + +- name: Disable NetBIOS on Ethernet2 + community.windows.win_netbios: + state: disabled + adapter_names: + - Ethernet2 + +- name: Enable NetBIOS on Public and Backup adapters + community.windows.win_netbios: + state: enabled + adapter_names: + - Public + - Backup + +- name: Set NetBIOS to system default on all adapters + community.windows.win_netbios: + state: default +''' + +RETURN = r''' +reboot_required: + description: Boolean value stating whether a system reboot is required. + returned: always + type: bool + sample: true +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_nssm.ps1 b/ansible_collections/community/windows/plugins/modules/win_nssm.ps1 new file mode 100644 index 000000000..3b951cf19 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_nssm.ps1 @@ -0,0 +1,604 @@ +#!powershell + +# Copyright: (c) 2015, George Frank <george@georgefrank.net> +# Copyright: (c) 2015, Adam Keech <akeech@chathamfinancial.com> +# Copyright: (c) 2015, Hans-Joachim Kliemeck <git@kliemeck.de> +# Copyright: (c) 2019, Kevin Subileau (@ksubileau) +# 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.ArgvParser +#Requires -Module Ansible.ModuleUtils.CommandUtil + +$ErrorActionPreference = "Stop" + +$start_modes_map = @{ + "auto" = "SERVICE_AUTO_START" + "delayed" = "SERVICE_DELAYED_AUTO_START" + "manual" = "SERVICE_DEMAND_START" + "disabled" = "SERVICE_DISABLED" +} + +$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 + +$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true +$state_options = "present", "absent", "started", "stopped", "restarted" +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset $state_options -resultobj $result +$display_name = Get-AnsibleParam -obj $params -name 'display_name' -type 'str' +$description = Get-AnsibleParam -obj $params -name 'description' -type 'str' + +$application = Get-AnsibleParam -obj $params -name "application" -type "path" +$appDirectory = Get-AnsibleParam -obj $params -name "working_directory" -aliases "app_directory", "chdir" -type "path" +$appParameters = Get-AnsibleParam -obj $params -name "app_parameters" +$appArguments = Get-AnsibleParam -obj $params -name "arguments" -aliases "app_parameters_free_form" + +$stdoutFile = Get-AnsibleParam -obj $params -name "stdout_file" -type "path" +$stderrFile = Get-AnsibleParam -obj $params -name "stderr_file" -type "path" + +$executable = Get-AnsibleParam -obj $params -name "executable" -type "path" -default "nssm.exe" + +$app_env = Get-AnsibleParam -obj $params -name "app_environment" -type "dict" + +$app_rotate_bytes = Get-AnsibleParam -obj $params -name "app_rotate_bytes" -type "int" -default 104858 +$app_rotate_online = Get-AnsibleParam -obj $params -name "app_rotate_online" -type "int" -default 0 -validateset 0, 1 +$app_stop_method_console = Get-AnsibleParam -obj $params -name "app_stop_method_console" -type "int" +$app_stop_method_skip = Get-AnsibleParam -obj $params -name "app_stop_method_skip" -type "int" -validateset 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 + +# Deprecated options, will be removed in a major release after 2021-07-01. +$startMode = Get-AnsibleParam -obj $params -name "start_mode" -type "str" -default "auto" -validateset $start_modes_map.Keys -resultobj $result +$dependencies = Get-AnsibleParam -obj $params -name "dependencies" -type "list" +$user = Get-AnsibleParam -obj $params -name "username" -type "str" -aliases "user" +$password = Get-AnsibleParam -obj $params -name "password" -type "str" + +$result = @{ + changed = $false +} +$diff_text = $null + +function Invoke-NssmCommand { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, ValueFromRemainingArguments = $true)] + [string[]]$arguments + ) + + $command = Argv-ToString -arguments (@($executable) + $arguments) + $result = Run-Command -command $command + + $result.arguments = $command + + return $result +} + +function Get-NssmServiceStatus { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$service + ) + + return Invoke-NssmCommand -arguments @("status", $service) +} + +function Get-NssmServiceParameter { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$service, + [Parameter(Mandatory = $true)] + [Alias("param")] + [string]$parameter, + [Parameter(Mandatory = $false)] + [string]$subparameter + ) + + $arguments = @("get", $service, $parameter) + if ($subparameter -ne "") { + $arguments += $subparameter + } + return Invoke-NssmCommand -arguments $arguments +} + +function Set-NssmServiceParameter { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$service, + [Parameter(Mandatory = $true)] + [string]$parameter, + [Parameter(Mandatory = $true, ValueFromRemainingArguments = $true)] + [Alias("value")] + [string[]]$arguments + ) + + return Invoke-NssmCommand -arguments (@("set", $service, $parameter) + $arguments) +} + +function Reset-NssmServiceParameter { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$service, + [Parameter(Mandatory = $true)] + [Alias("param")] + [string]$parameter + ) + + return Invoke-NssmCommand -arguments @("reset", $service, $parameter) +} + +function Update-NssmServiceParameter { + <# + .SYNOPSIS + A generic cmdlet to idempotently set a nssm service parameter. + .PARAMETER service + [String] The service name + .PARAMETER parameter + [String] The name of the nssm parameter to set. + .PARAMETER arguments + [String[]] Target value (or list of value) or array of arguments to pass to the 'nssm set' command. + .PARAMETER compare + [scriptblock] An optionnal idempotency check scriptblock that must return true when + the current value is equal to the desired value. Usefull when 'nssm get' doesn't return + the same value as 'nssm set' takes in argument, like for the ObjectName parameter. + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [Parameter(Mandatory = $true)] + [string]$service, + + [Parameter(Mandatory = $true)] + [string]$parameter, + + [Parameter(Mandatory = $true, ValueFromRemainingArguments = $true)] + [AllowEmptyString()] + [AllowNull()] + [Alias("value")] + [string[]]$arguments, + + [Parameter()] + [scriptblock]$compare = { param($actual, $expected) @(Compare-Object -ReferenceObject $actual -DifferenceObject $expected).Length -eq 0 } + ) + + if ($null -eq $arguments) { return } + $arguments = @($arguments | Where-Object { $_ -ne '' }) + + $nssm_result = Get-NssmServiceParameter -service $service -parameter $parameter + + if ($nssm_result.rc -ne 0) { + $result.nssm_error_cmd = $nssm_result.arguments + $result.nssm_error_log = $nssm_result.stderr + Fail-Json -obj $result -message "Error retrieving $parameter for service ""$service""" + } + + $current_values = @($nssm_result.stdout.split("`n`r") | Where-Object { $_ -ne '' }) + + if (-not $compare.Invoke($current_values, $arguments)) { + if ($PSCmdlet.ShouldProcess($service, "Update '$parameter' parameter")) { + if ($arguments.Count -gt 0) { + $nssm_result = Set-NssmServiceParameter -service $service -parameter $parameter -arguments $arguments + } + else { + $nssm_result = Reset-NssmServiceParameter -service $service -parameter $parameter + } + + if ($nssm_result.rc -ne 0) { + $result.nssm_error_cmd = $nssm_result.arguments + $result.nssm_error_log = $nssm_result.stderr + Fail-Json -obj $result -message "Error setting $parameter for service ""$service""" + } + } + + $script:diff_text += "-$parameter = $($current_values -join ', ')`n+$parameter = $($arguments -join ', ')`n" + $result.changed_by = $parameter + $result.changed = $true + } +} + +function Test-NssmServiceExist { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$service + ) + + return [bool](Get-Service -Name $service -ErrorAction SilentlyContinue) +} + +function Invoke-NssmStart { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$service + ) + + $nssm_result = Invoke-NssmCommand -arguments @("start", $service) + + if ($nssm_result.rc -ne 0) { + $result.nssm_error_cmd = $nssm_result.arguments + $result.nssm_error_log = $nssm_result.stderr + Fail-Json -obj $result -message "Error starting service ""$service""" + } +} + +function Invoke-NssmStop { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$service + ) + + $nssm_result = Invoke-NssmCommand -arguments @("stop", $service) + + if ($nssm_result.rc -ne 0) { + $result.nssm_error_cmd = $nssm_result.arguments + $result.nssm_error_log = $nssm_result.stderr + Fail-Json -obj $result -message "Error stopping service ""$service""" + } +} + +function Start-NssmService { + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [Parameter(Mandatory = $true)] + [string]$service + ) + + $currentStatus = Get-NssmServiceStatus -service $service + + if ($currentStatus.rc -ne 0) { + $result.nssm_error_cmd = $currentStatus.arguments + $result.nssm_error_log = $currentStatus.stderr + Fail-Json -obj $result -message "Error starting service ""$service""" + } + + if ($currentStatus.stdout -notlike "*SERVICE_RUNNING*") { + if ($PSCmdlet.ShouldProcess($service, "Start service")) { + switch -wildcard ($currentStatus.stdout) { + "*SERVICE_STOPPED*" { Invoke-NssmStart -service $service } + + "*SERVICE_CONTINUE_PENDING*" { Invoke-NssmStop -service $service; Invoke-NssmStart -service $service } + "*SERVICE_PAUSE_PENDING*" { Invoke-NssmStop -service $service; Invoke-NssmStart -service $service } + "*SERVICE_PAUSED*" { Invoke-NssmStop -service $service; Invoke-NssmStart -service $service } + "*SERVICE_START_PENDING*" { Invoke-NssmStop -service $service; Invoke-NssmStart -service $service } + "*SERVICE_STOP_PENDING*" { Invoke-NssmStop -service $service; Invoke-NssmStart -service $service } + } + } + + $result.changed_by = "start_service" + $result.changed = $true + } +} + +function Stop-NssmService { + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [Parameter(Mandatory = $true)] + [string]$service + ) + + $currentStatus = Get-NssmServiceStatus -service $service + + if ($currentStatus.rc -ne 0) { + $result.nssm_error_cmd = $currentStatus.arguments + $result.nssm_error_log = $currentStatus.stderr + Fail-Json -obj $result -message "Error stopping service ""$service""" + } + + if ($currentStatus.stdout -notlike "*SERVICE_STOPPED*") { + if ($PSCmdlet.ShouldProcess($service, "Stop service")) { + Invoke-NssmStop -service $service + } + + $result.changed_by = "stop_service" + $result.changed = $true + } +} + +function Add-DepByDate { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [String]$Message, + + [Parameter(Mandatory = $true)] + [String]$Date + ) + + # Legacy doesn't natively support deprecate by date, need to do this manually until we use Ansible.Basic + if (-not $result.ContainsKey('deprecations')) { + $result.deprecations = @() + } + $result.deprecations += @{ + msg = $Message + date = $Date + collection_name = "community.windows" + } +} + +Function ConvertTo-NormalizedUser { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [String]$InputObject + ) + + $systemSid = [System.Security.Principal.SecurityIdentifier]'S-1-5-18' + + # Try to get the SID from the raw value or with LocalSystem (what services consider to be SYSTEM). + try { + $sid = New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList $InputObject + } + catch [ArgumentException] { + if ($InputObject -eq "LocalSystem") { + $sid = $systemSid + } + } + + if (-not $sid) { + $candidates = @(if ($InputObject.Contains('\')) { + $nameSplit = $InputObject.Split('\', 2) + + if ($nameSplit[0] -eq '.') { + # If the domain portion is . try using the hostname then falling back to just the username. + # Usually the hostname just works except when running on a DC where it's a domain account + # where looking up just the username should work. + , @($env:COMPUTERNAME, $nameSplit[1]) + $nameSplit[1] + } + else { + , $nameSplit + } + } + else { + $InputObject + }) + + $sid = for ($i = 0; $i -lt $candidates.Length; $i++) { + $candidate = $candidates[$i] + $ntAccount = New-Object -TypeName System.Security.Principal.NTAccount -ArgumentList $candidate + try { + $ntAccount.Translate([System.Security.Principal.SecurityIdentifier]) + break + } + catch [System.Security.Principal.IdentityNotMappedException] { + if ($i -eq ($candidates.Length - 1)) { + throw + } + continue + } + } + } + + if ($sid -eq $systemSid) { + "LocalSystem" + } + else { + $sid.Translate([System.Security.Principal.NTAccount]).Value + } +} + +if (($null -ne $appParameters) -and ($null -ne $appArguments)) { + Fail-Json $result "'app_parameters' and 'arguments' are mutually exclusive but have both been set." +} + +# Backward compatibility for old parameters style. Remove the block bellow in 2.12 +if ($null -ne $appParameters) { + $dep = @{ + Message = "The parameter 'app_parameters' will be removed soon, use 'arguments' instead" + Date = "2022-07-01" + } + Add-DepByDate @dep + + if ($appParameters -isnot [string]) { + Fail-Json -obj $result -message "The app_parameters parameter must be a string representing a dictionary." + } + + # Convert dict-as-string form to list + $escapedAppParameters = $appParameters.TrimStart("@").TrimStart("{").TrimEnd("}").Replace("; ", "`n").Replace("\", "\\") + $appParametersHash = ConvertFrom-StringData -StringData $escapedAppParameters + + $appParamsArray = @() + $appParametersHash.GetEnumerator() | Foreach-Object { + if ($_.Name -ne "_") { + $appParamsArray += $_.Name + } + $appParamsArray += $_.Value + } + $appArguments = @($appParamsArray) + + # The rest of the code should use only the new $appArguments variable +} + +if ($state -ne 'absent') { + if ($null -eq $application) { + Fail-Json -obj $result -message "The application parameter must be defined when the state is not absent." + } + + if (-not (Test-Path -LiteralPath $application -PathType Leaf)) { + Fail-Json -obj $result -message "The application specified ""$application"" does not exist on the host." + } + + if ($null -eq $appDirectory) { + $appDirectory = (Get-Item -LiteralPath $application).DirectoryName + } + + if ($user) { + $user = ConvertTo-NormalizedUser -InputObject $user + if ( + $user -in @( + (ConvertTo-NormalizedUser -InputObject 'S-1-5-18'), # SYSTEM + (ConvertTo-NormalizedUser -InputObject 'S-1-5-19'), # LOCAL SERVICE + (ConvertTo-NormalizedUser -InputObject 'S-1-5-20') # NETWORK SERVICE + ) + ) { + # These accounts have no password (NSSM expects nothing) + $password = "" + } + elseif ($user.EndsWith('$')) { + # While a gMSA doesn't have a password NSSM will fail with no password so we set a dummy value. The service + # still starts up properly with this so SCManager handles this nicely. + $password = "gsma_password" + } + elseif (-not $password) { + # Any other account requires a password here. + Fail-Json -obj $result -message "User without password is informed for service ""$name""" + } + } +} + + +$service_exists = Test-NssmServiceExist -service $name + +if ($state -eq 'absent') { + if ($service_exists) { + if (-not $check_mode) { + if ((Get-Service -Name $name).Status -ne "Stopped") { + $nssm_result = Invoke-NssmStop -service $name + } + + $nssm_result = Invoke-NssmCommand -arguments @("remove", $name, "confirm") + + if ($nssm_result.rc -ne 0) { + $result.nssm_error_cmd = $nssm_result.arguments + $result.nssm_error_log = $nssm_result.stderr + Fail-Json -obj $result -message "Error removing service ""$name""" + } + } + + $diff_text += "-[$name]" + $result.changed_by = "remove_service" + $result.changed = $true + } +} +else { + $diff_text_added_prefix = '' + if (-not $service_exists) { + if (-not $check_mode) { + $nssm_result = Invoke-NssmCommand -arguments @("install", $name, $application) + + if ($nssm_result.rc -ne 0) { + $result.nssm_error_cmd = $nssm_result.arguments + $result.nssm_error_log = $nssm_result.stderr + Fail-Json -obj $result -message "Error installing service ""$name""" + } + $service_exists = $true + } + + $diff_text_added_prefix = '+' + $result.changed_by = "install_service" + $result.changed = $true + } + + $diff_text += "$diff_text_added_prefix[$name]`n" + + # We cannot configure a service that was created above in check mode as it won't actually exist + if ($service_exists) { + $common_params = @{ + service = $name + WhatIf = $check_mode + } + + Update-NssmServiceParameter -parameter "Application" -value $application @common_params + Update-NssmServiceParameter -parameter "DisplayName" -value $display_name @common_params + Update-NssmServiceParameter -parameter "Description" -value $description @common_params + + Update-NssmServiceParameter -parameter "AppDirectory" -value $appDirectory @common_params + + + if ($null -ne $appArguments) { + $singleLineParams = "" + if ($appArguments -is [array]) { + $singleLineParams = Argv-ToString -arguments $appArguments + } + else { + $singleLineParams = $appArguments.ToString() + } + + $result.nssm_app_parameters = $appArguments + $result.nssm_single_line_app_parameters = $singleLineParams + + Update-NssmServiceParameter -parameter "AppParameters" -value $singleLineParams @common_params + } + + + Update-NssmServiceParameter -parameter "AppStdout" -value $stdoutFile @common_params + Update-NssmServiceParameter -parameter "AppStderr" -value $stderrFile @common_params + + # set app environment, only do this for now when explicitly requested by caller to + # avoid breaking playbooks which use another / custom scheme for configuring app_env + if ($null -ne $app_env) { + # note: convert app_env dictionary to list of strings in the form key=value and pass that a long as value + $app_env_str = $app_env.GetEnumerator() | ForEach-Object { "$($_.Name)=$($_.Value)" } + + # note: this is important here to make an empty envvar set working properly (in the sense that appenv is reset) + if ($null -eq $app_env_str) { + $app_env_str = '' + } + + Update-NssmServiceParameter -parameter "AppEnvironmentExtra" -value $app_env_str @common_params + } + + ### + # Setup file rotation so we don't accidentally consume too much disk + ### + + #set files to overwrite + Update-NssmServiceParameter -parameter "AppStdoutCreationDisposition" -value 2 @common_params + Update-NssmServiceParameter -parameter "AppStderrCreationDisposition" -value 2 @common_params + + #enable file rotation + Update-NssmServiceParameter -parameter "AppRotateFiles" -value 1 @common_params + + #don't rotate until the service restarts + Update-NssmServiceParameter -parameter "AppRotateOnline" -value $app_rotate_online @common_params + + #both of the below conditions must be met before rotation will happen + #minimum age before rotating + Update-NssmServiceParameter -parameter "AppRotateSeconds" -value 86400 @common_params + + #minimum size before rotating + Update-NssmServiceParameter -parameter "AppRotateBytes" -value $app_rotate_bytes @common_params + + Update-NssmServiceParameter -parameter "DependOnService" -arguments $dependencies @common_params + if ($user) { + # Use custom compare callback to test only the username (and not the password) + Update-NssmServiceParameter -parameter "ObjectName" -arguments @($user, $password) -compare { + param($actual, $expected) + + $actualUser = ConvertTo-NormalizedUser -InputObject $actual[0] + $expectedUser = ConvertTo-NormalizedUser -InputObject $expected[0] + + $actualUser -eq $expectedUser + } @common_params + } + $mappedMode = $start_modes_map.$startMode + Update-NssmServiceParameter -parameter "Start" -value $mappedMode @common_params + if ($state -in "stopped", "restarted") { + Stop-NssmService @common_params + } + + if ($state -in "started", "restarted") { + Start-NssmService @common_params + } + + # Added per users` requests + if ($null -ne $app_stop_method_console) { + Update-NssmServiceParameter -parameter "AppStopMethodConsole" -value $app_stop_method_console @common_params + } + + if ($null -ne $app_stop_method_skip) { + Update-NssmServiceParameter -parameter "AppStopMethodSkip" -value $app_stop_method_skip @common_params + } + } +} + +if ($diff_mode -and $result.changed -eq $true) { + $result.diff = @{ + prepared = $diff_text + } +} + +Exit-Json $result diff --git a/ansible_collections/community/windows/plugins/modules/win_nssm.py b/ansible_collections/community/windows/plugins/modules/win_nssm.py new file mode 100644 index 000000000..d79f638b8 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_nssm.py @@ -0,0 +1,216 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2015, Heyo +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_nssm +short_description: Install a service using NSSM +description: + - Install a Windows service using the NSSM wrapper. + - NSSM is a service helper which doesn't suck. See U(https://nssm.cc/) for more information. +requirements: + - "nssm >= 2.24.0 # (install via M(chocolatey.chocolatey.win_chocolatey)) C(win_chocolatey: name=nssm)" +options: + name: + description: + - Name of the service to operate on. + type: str + required: true + state: + description: + - State of the service on the system. + type: str + choices: [ absent, present, started, stopped, restarted ] + default: present + application: + description: + - The application binary to run as a service + - Required when I(state) is C(present), C(started), C(stopped), or C(restarted). + type: path + executable: + description: + - The location of the NSSM utility (in case it is not located in your PATH). + type: path + default: nssm.exe + description: + description: + - The description to set for the service. + type: str + display_name: + description: + - The display name to set for the service. + type: str + working_directory: + description: + - The working directory to run the service executable from (defaults to the directory containing the application binary) + type: path + aliases: [ app_directory, chdir ] + stdout_file: + description: + - Path to receive output. + type: path + stderr_file: + description: + - Path to receive error output. + type: path + app_parameters: + description: + - A string representing a dictionary of parameters to be passed to the application when it starts. + - DEPRECATED since v2.8, please use I(arguments) instead. + - This is mutually exclusive with I(arguments). + type: str + arguments: + description: + - Parameters to be passed to the application when it starts. + - This can be either a simple string or a list. + - This is mutually exclusive with I(app_parameters). + aliases: [ app_parameters_free_form ] + type: str + dependencies: + description: + - Service dependencies that has to be started to trigger startup, separated by comma. + type: list + elements: str + username: + description: + - User to be used for service startup. + - Group managed service accounts must end with C($). + - Before C(1.8.0), this parameter was just C(user). + type: str + aliases: + - user + password: + description: + - Password to be used for service startup. + - This is not required for the well known service accounts and group managed service accounts. + type: str + start_mode: + description: + - If C(auto) is selected, the service will start at bootup. + - C(delayed) causes a delayed but automatic start after boot. + - C(manual) means that the service will start only when another service needs it. + - C(disabled) means that the service will stay off, regardless if it is needed or not. + type: str + choices: [ auto, delayed, disabled, manual ] + default: auto + app_environment: + description: + - Key/Value pairs which will be added to the environment of the service application. + type: dict + version_added: 1.2.0 + app_rotate_bytes: + description: + - NSSM will not rotate any file which is smaller than the configured number of bytes. + type: int + default: 104858 + app_rotate_online: + description: + - If set to 1, nssm can rotate files which grow to the configured file size limit while the service is running. + type: int + choices: + - 0 + - 1 + default: 0 + app_stop_method_console: + description: + - Time to wait after sending Control-C. + type: int + app_stop_method_skip: + description: + - To disable service shutdown methods, set to the sum of one or more of the numbers + - 1 - Don't send Control-C to the console. + - 2 - Don't send WM_CLOSE to windows. + - 4 - Don't send WM_QUIT to threads. + - 8 - Don't call TerminateProcess(). + type: int + choices: + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + - 8 + - 9 + - 10 + - 11 + - 12 + - 13 + - 14 + - 15 +seealso: + - module: ansible.windows.win_service +notes: + - The service will NOT be started after its creation when C(state=present). + - Once the service is created, you can use the M(ansible.windows.win_service) module to start it or configure + some additionals properties, such as its startup type, dependencies, service account, and so on. +author: + - Adam Keech (@smadam813) + - George Frank (@georgefrank) + - Hans-Joachim Kliemeck (@h0nIg) + - Michael Wild (@themiwi) + - Kevin Subileau (@ksubileau) + - Shachaf Goldstein (@Shachaf92) +''' + +EXAMPLES = r''' +- name: Install the foo service + community.windows.win_nssm: + name: foo + application: C:\windows\foo.exe + +# This will yield the following command: C:\windows\foo.exe bar "true" +- name: Install the Consul service with a list of parameters + community.windows.win_nssm: + name: Consul + application: C:\consul\consul.exe + arguments: + - agent + - -config-dir=C:\consul\config + +# This is strictly equivalent to the previous example +- name: Install the Consul service with an arbitrary string of parameters + community.windows.win_nssm: + name: Consul + application: C:\consul\consul.exe + arguments: agent -config-dir=C:\consul\config + + +# Install the foo service, and then configure and start it with win_service +- name: Install the foo service, redirecting stdout and stderr to the same file + community.windows.win_nssm: + name: foo + application: C:\windows\foo.exe + stdout_file: C:\windows\foo.log + stderr_file: C:\windows\foo.log + +- name: Configure and start the foo service using win_service + ansible.windows.win_service: + name: foo + dependencies: [ adf, tcpip ] + username: foouser + password: secret + start_mode: manual + state: started + +- name: Install a script based service and define custom environment variables + community.windows.win_nssm: + name: <ServiceName> + application: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe + arguments: + - <path-to-script> + - <script arg> + app_environment: + AUTH_TOKEN: <token value> + SERVER_URL: https://example.com + PATH: "<path-prepends>;{{ ansible_env.PATH }};<path-appends>" + +- name: Remove the foo service + community.windows.win_nssm: + name: foo + state: absent +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_pagefile.ps1 b/ansible_collections/community/windows/plugins/modules/win_pagefile.ps1 new file mode 100644 index 000000000..627d4e461 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_pagefile.ps1 @@ -0,0 +1,243 @@ +#!powershell + +# Copyright: (c) 2017, Liran Nisanov <lirannis@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#Requires -Module Ansible.ModuleUtils.Legacy + +######## + +Function Remove-Pagefile { + [CmdletBinding(SupportsShouldProcess)] + param( + $path + ) + Get-CIMInstance Win32_PageFileSetting | Where-Object { $_.Name -eq $path } | ForEach-Object { + if ($PSCmdlet.ShouldProcess($Path, "remove pagefile")) { + $_ | Remove-CIMInstance + } + } +} + +Function Get-Pagefile($path) { + Get-CIMInstance Win32_PageFileSetting | Where-Object { $_.Name -eq $path } +} + +######## + +$params = Parse-Args $args -supports_check_mode $true +$check_mode = Get-AnsibleParam -obj $params -name '_ansible_check_mode' -type 'bool' -default $false + +$automatic = Get-AnsibleParam -obj $params -name "automatic" -type "bool" +$drive = Get-AnsibleParam -obj $params -name "drive" -type "str" +$fullPath = $drive + ":\pagefile.sys" +$initialSize = Get-AnsibleParam -obj $params -name "initial_size" -type "int" +$maximumSize = Get-AnsibleParam -obj $params -name "maximum_size" -type "int" +$override = Get-AnsibleParam -obj $params -name "override" -type "bool" -default $true +$removeAll = Get-AnsibleParam -obj $params -name "remove_all" -type "bool" -default $false +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "query" -validateset "present", "absent", "query" +$systemManaged = Get-AnsibleParam -obj $params -name "system_managed" -type "bool" -default $false +$test_path = Get-AnsibleParam -obj $params -name "test_path" -type "bool" -default $true + +$result = @{ + changed = $false +} + +if ($removeAll) { + $currentPageFiles = Get-CIMInstance Win32_PageFileSetting + if ($null -ne $currentPageFiles) { + $currentPageFiles | Remove-CIMInstance -WhatIf:$check_mode > $null + $result.changed = $true + } +} + +$regPath = "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management" + +if ($null -ne $automatic) { + # change autmoatic managed pagefile + try { + $computerSystem = Get-CIMInstance -Class win32_computersystem + } + catch { + Fail-Json $result "Failed to query WMI computer system object $($_.Exception.Message)" + } + if ($computerSystem.AutomaticManagedPagefile -ne $automatic) { + if (-not $check_mode) { + try { + $computerSystem | Set-CimInstance -Property @{automaticmanagedpagefile = "$automatic" } > $null + } + catch { + Fail-Json $result "Failed to set AutomaticManagedPagefile $($_.Exception.Message)" + } + } + $result.changed = $true + } +} + +if ($state -eq "absent") { + # Remove pagefile + if ($null -ne (Get-Pagefile $fullPath)) { + try { + Remove-Pagefile $fullPath -whatif:$check_mode + } + catch { + Fail-Json $result "Failed to remove pagefile $($_.Exception.Message)" + } + $result.changed = $true + } +} +elseif ($state -eq "present") { + # Remove current pagefile + if ($override) { + if ($null -ne (Get-Pagefile $fullPath)) { + try { + Remove-Pagefile $fullPath -whatif:$check_mode + } + catch { + Fail-Json $result "Failed to remove current pagefile $($_.Exception.Message)" + } + $result.changed = $true + } + } + + # Make sure drive is accessible + if (($test_path) -and (-not (Test-Path -LiteralPath "${drive}:"))) { + Fail-Json $result "Unable to access '${drive}:' drive" + } + + $curPagefile = Get-Pagefile $fullPath + + # Set pagefile + if ($null -eq $curPagefile) { + try { + $pagefile = New-CIMInstance -Class Win32_PageFileSetting -Arguments @{name = $fullPath; } -WhatIf:$check_mode + } + catch { + Fail-Json $result "Failed to create pagefile $($_.Exception.Message)" + } + if (-not ($systemManaged -or $check_mode)) { + try { + $pagefile | Set-CimInstance -Property @{ InitialSize = $initialSize; MaximumSize = $maximumSize } + } + catch { + $originalExceptionMessage = $($_.Exception.Message) + # Try workaround before failing + try { + Remove-Pagefile $fullPath -whatif:$check_mode + } + catch { + Fail-Json $result "Failed to remove pagefile before workaround $($_.Exception.Message) Original exception: $originalExceptionMessage" + } + try { + $pagingFilesValues = (Get-ItemProperty -LiteralPath $regPath).PagingFiles + } + catch { + $msg = -join @( + "Failed to get pagefile settings from the registry for workaround $($_.Exception.Message) " + "Original exception: $originalExceptionMessage" + ) + Fail-Json $result $msg + } + $pagingFilesValues += "$fullPath $initialSize $maximumSize" + try { + Set-ItemProperty -LiteralPath $regPath "PagingFiles" $pagingFilesValues + } + catch { + $msg = -join @( + "Failed to set pagefile settings to the registry for workaround $($_.Exception.Message) " + "Original exception: $originalExceptionMessage" + ) + Fail-Json $result $msg + } + } + } + $result.changed = $true + } + else { + if ((-not $check_mode) -and + -not ($systemManaged) -and + -not ( ($curPagefile.InitialSize -eq 0) -and ($curPagefile.maximumSize -eq 0) ) -and + ( ($curPagefile.InitialSize -ne $initialSize) -or ($curPagefile.maximumSize -ne $maximumSize) ) + ) { + $curPagefile.InitialSize = $initialSize + $curPagefile.MaximumSize = $maximumSize + try { + $curPagefile.Put() | out-null + } + catch { + $originalExceptionMessage = $($_.Exception.Message) + # Try workaround before failing + try { + Remove-Pagefile $fullPath -whatif:$check_mode + } + catch { + Fail-Json $result "Failed to remove pagefile before workaround $($_.Exception.Message) Original exception: $originalExceptionMessage" + } + try { + $pagingFilesValues = (Get-ItemProperty -LiteralPath $regPath).PagingFiles + } + catch { + $msg = -join @( + "Failed to get pagefile settings from the registry for workaround $($_.Exception.Message) " + "Original exception: $originalExceptionMessage" + ) + Fail-Json $result $msg + } + $pagingFilesValues += "$fullPath $initialSize $maximumSize" + try { + + Set-ItemProperty -LiteralPath $regPath -Name "PagingFiles" -Value $pagingFilesValues + } + catch { + $msg = -join @( + "Failed to set pagefile settings to the registry for workaround $($_.Exception.Message) " + "Original exception: $originalExceptionMessage" + ) + Fail-Json $result $msg + } + } + $result.changed = $true + } + } +} +elseif ($state -eq "query") { + $result.pagefiles = @() + + if ($null -eq $drive) { + try { + $pagefiles = Get-CIMInstance Win32_PageFileSetting + } + catch { + Fail-Json $result "Failed to query all pagefiles $($_.Exception.Message)" + } + } + else { + try { + $pagefiles = Get-Pagefile $fullPath + } + catch { + Fail-Json $result "Failed to query specific pagefile $($_.Exception.Message)" + } + } + + # Get all pagefiles + foreach ($currentPagefile in $pagefiles) { + $currentPagefileObject = @{ + name = $currentPagefile.Name + initial_size = $currentPagefile.InitialSize + maximum_size = $currentPagefile.MaximumSize + caption = $currentPagefile.Caption + description = $currentPagefile.Description + } + $result.pagefiles += , $currentPagefileObject + } + + # Get automatic managed pagefile state + try { + $result.automatic_managed_pagefiles = (Get-CIMInstance -Class win32_computersystem).AutomaticManagedPagefile + } + catch { + Fail-Json $result "Failed to query automatic managed pagefile state $($_.Exception.Message)" + } +} +Exit-Json $result diff --git a/ansible_collections/community/windows/plugins/modules/win_pagefile.py b/ansible_collections/community/windows/plugins/modules/win_pagefile.py new file mode 100644 index 000000000..c8b752203 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_pagefile.py @@ -0,0 +1,131 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2017, Liran Nisanov <lirannis@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_pagefile +short_description: Query or change pagefile configuration +description: + - Query current pagefile configuration. + - Enable/Disable AutomaticManagedPagefile. + - Create new or override pagefile configuration. +options: + drive: + description: + - The drive of the pagefile. + type: str + initial_size: + description: + - The initial size of the pagefile in megabytes. + type: int + maximum_size: + description: + - The maximum size of the pagefile in megabytes. + type: int + override: + description: + - Override the current pagefile on the drive. + type: bool + default: yes + system_managed: + description: + - Configures current pagefile to be managed by the system. + type: bool + default: no + automatic: + description: + - Configures AutomaticManagedPagefile for the entire system. + type: bool + remove_all: + description: + - Remove all pagefiles in the system, not including automatic managed. + type: bool + default: no + test_path: + description: + - Use Test-Path on the drive to make sure the drive is accessible before creating the pagefile. + type: bool + default: yes + state: + description: + - State of the pagefile. + type: str + choices: [ absent, present, query ] + default: query +notes: +- There is difference between automatic managed pagefiles that configured once for the entire system and system managed pagefile that configured per pagefile. +- InitialSize 0 and MaximumSize 0 means the pagefile is managed by the system. +- Value out of range exception may be caused by several different issues, two common problems - No such drive, Pagefile size is too small. +- Setting a pagefile when AutomaticManagedPagefile is on will disable the AutomaticManagedPagefile. +author: +- Liran Nisanov (@LiranNis) +''' + +EXAMPLES = r''' +- name: Query pagefiles configuration + community.windows.win_pagefile: + +- name: Query C pagefile + community.windows.win_pagefile: + drive: C + +- name: Set C pagefile, don't override if exists + community.windows.win_pagefile: + drive: C + initial_size: 1024 + maximum_size: 1024 + override: no + state: present + +- name: Set C pagefile, override if exists + community.windows.win_pagefile: + drive: C + initial_size: 1024 + maximum_size: 1024 + state: present + +- name: Remove C pagefile + community.windows.win_pagefile: + drive: C + state: absent + +- name: Remove all current pagefiles, enable AutomaticManagedPagefile and query at the end + community.windows.win_pagefile: + remove_all: yes + automatic: yes + +- name: Remove all pagefiles disable AutomaticManagedPagefile and set C pagefile + community.windows.win_pagefile: + drive: C + initial_size: 2048 + maximum_size: 2048 + remove_all: yes + automatic: no + state: present + +- name: Set D pagefile, override if exists + community.windows.win_pagefile: + drive: d + initial_size: 1024 + maximum_size: 1024 + state: present +''' + +RETURN = r''' +automatic_managed_pagefiles: + description: Whether the pagefiles is automatically managed. + returned: When state is query. + type: bool + sample: true +pagefiles: + description: Contains caption, description, initial_size, maximum_size and name for each pagefile in the system. + returned: When state is query. + type: list + sample: + [{"caption": "c:\\ 'pagefile.sys'", "description": "'pagefile.sys' @ c:\\", "initial_size": 2048, "maximum_size": 2048, "name": "c:\\pagefile.sys"}, + {"caption": "d:\\ 'pagefile.sys'", "description": "'pagefile.sys' @ d:\\", "initial_size": 1024, "maximum_size": 1024, "name": "d:\\pagefile.sys"}] + +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_partition.ps1 b/ansible_collections/community/windows/plugins/modules/win_partition.ps1 new file mode 100644 index 000000000..6e4517690 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_partition.ps1 @@ -0,0 +1,371 @@ +#!powershell + +# Copyright: (c) 2018, Varun Chopra (@chopraaa) <v@chopraaa.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic +#AnsibleRequires -OSVersion 6.2 + +Set-StrictMode -Version 2 + +$ErrorActionPreference = "Stop" + +$spec = @{ + options = @{ + state = @{ type = "str"; choices = "absent", "present"; default = "present" } + drive_letter = @{ type = "str" } + disk_number = @{ type = "int" } + partition_number = @{ type = "int" } + partition_size = @{ type = "str" } + read_only = @{ type = "bool" } + active = @{ type = "bool" } + hidden = @{ type = "bool" } + offline = @{ type = "bool" } + mbr_type = @{ type = "str"; choices = "fat12", "fat16", "extended", "huge", "ifs", "fat32" } + gpt_type = @{ type = "str"; choices = "system_partition", "microsoft_reserved", "basic_data", "microsoft_recovery" } + } + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$state = $module.Params.state +$drive_letter = $module.Params.drive_letter +$disk_number = $module.Params.disk_number +$partition_number = $module.Params.partition_number +$partition_size = $module.Params.partition_size +$read_only = $module.Params.read_only +$active = $module.Params.active +$hidden = $module.Params.hidden +$offline = $module.Params.offline +$mbr_type = $module.Params.mbr_type +$gpt_type = $module.Params.gpt_type + +$size_is_maximum = $false +$ansible_partition = $false +$ansible_partition_size = $null +$partition_style = $null + +$gpt_styles = @{ + system_partition = "{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}" + microsoft_reserved = "{e3c9e316-0b5c-4db8-817d-f92df00215ae}" + basic_data = "{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}" + microsoft_recovery = "{de94bba4-06d1-4d40-a16a-bfd50179d6ac}" +} + +$mbr_styles = @{ + fat12 = 1 + fat16 = 4 + extended = 5 + huge = 6 + ifs = 7 + fat32 = 12 +} + +function Convert-SizeToByte { + param( + $Size, + $Units + ) + + switch ($Units) { + "B" { return 1 * $Size } + "KB" { return 1000 * $Size } + "KiB" { return 1024 * $Size } + "MB" { return [Math]::Pow(1000, 2) * $Size } + "MiB" { return [Math]::Pow(1024, 2) * $Size } + "GB" { return [Math]::Pow(1000, 3) * $Size } + "GiB" { return [Math]::Pow(1024, 3) * $Size } + "TB" { return [Math]::Pow(1000, 4) * $Size } + "TiB" { return [Math]::Pow(1024, 4) * $Size } + } +} + +if ($null -ne $partition_size) { + if ($partition_size -eq -1) { + $size_is_maximum = $true + } + elseif ($partition_size -match '^(?<Size>[0-9]+)[ ]*(?<Units>b|kb|kib|mb|mib|gb|gib|tb|tib)$') { + $ansible_partition_size = Convert-SizeToByte -Size $Matches.Size -Units $Matches.Units + } + else { + $module.FailJson("Invalid partition size. B, KB, KiB, MB, MiB, GB, GiB, TB, TiB are valid partition size units") + } +} + +# If partition_exists, we can change or delete it; otherwise we only need the disk to create a new partition +if ($null -ne $disk_number -and $null -ne $partition_number) { + $ansible_partition = Get-Partition -DiskNumber $disk_number -PartitionNumber $partition_number -ErrorAction SilentlyContinue +} +# Check if drive_letter is either auto-assigned or a character from A-Z +elseif ($drive_letter -and $drive_letter -ne "auto" -and -not ($disk_number -and $partition_number)) { + if ($drive_letter -match "^[a-zA-Z]$") { + $ansible_partition = Get-Partition -DriveLetter $drive_letter -ErrorAction SilentlyContinue + } + else { + $module.FailJson("Incorrect usage of drive_letter: specify a drive letter from A-Z or use 'auto' to automatically assign a drive letter") + } +} +elseif ($disk_number) { + try { + Get-Disk -Number $disk_number | Out-Null + } + catch { + $module.FailJson("Specified disk does not exist") + } +} +else { + $module.FailJson("You must provide disk_number, partition_number") +} + +# Partition can't have two partition styles +if ($null -ne $gpt_type -and $null -ne $mbr_type) { + $module.FailJson("Cannot specify both GPT and MBR partition styles. Check which partition style is supported by the disk") +} + +function New-AnsiblePartition { + param( + $DiskNumber, + $Letter, + $SizeMax, + $Size, + $MbrType, + $GptType, + $Style + ) + + $parameters = @{ + DiskNumber = $DiskNumber + } + + if ($null -ne $Letter) { + switch ($Letter) { + "auto" { + $parameters.Add("AssignDriveLetter", $True) + } + default { + $parameters.Add("DriveLetter", $Letter) + } + } + } + + if ($null -ne $Size) { + $parameters.Add("Size", $Size) + } + + if ($null -ne $MbrType) { + $parameters.Add("MbrType", $Style) + } + + if ($null -ne $GptType) { + $parameters.Add("GptType", $Style) + } + + try { + $new_partition = New-Partition @parameters + } + catch { + $module.FailJson("Unable to create a new partition: $($_.Exception.Message)", $_) + } + + return $new_partition +} + + +function Set-AnsiblePartitionState { + param( + $hidden, + $read_only, + $active, + $partition + ) + + $parameters = @{ + DiskNumber = $partition.DiskNumber + PartitionNumber = $partition.PartitionNumber + } + + if ($hidden -NotIn ($null, $partition.IsHidden)) { + $parameters.Add("IsHidden", $hidden) + } + + if ($read_only -NotIn ($null, $partition.IsReadOnly)) { + $parameters.Add("IsReadOnly", $read_only) + } + + if ($active -NotIn ($null, $partition.IsActive)) { + $parameters.Add("IsActive", $active) + } + + try { + Set-Partition @parameters + } + catch { + $module.FailJson("Error changing state of partition: $($_.Exception.Message)", $_) + } +} + + +if ($ansible_partition) { + if ($state -eq "absent") { + try { + $remove_params = @{ + DiskNumber = $ansible_partition.DiskNumber + PartitionNumber = $ansible_partition.PartitionNumber + Confirm = $false + WhatIf = $module.CheckMode + } + Remove-Partition @remove_params + } + catch { + $module.FailJson("There was an error removing the partition: $($_.Exception.Message)", $_) + } + $module.Result.changed = $true + } + else { + + if ($null -ne $gpt_type -and $gpt_styles.$gpt_type -ne $ansible_partition.GptType) { + $module.FailJson("gpt_type is not a valid parameter for existing partitions") + } + if ($null -ne $mbr_type -and $mbr_styles.$mbr_type -ne $ansible_partition.MbrType) { + $module.FailJson("mbr_type is not a valid parameter for existing partitions") + } + + if ($partition_size) { + try { + $get_params = @{ + DiskNumber = $ansible_partition.DiskNumber + PartitionNumber = $ansible_partition.PartitionNumber + } + $max_supported_size = (Get-PartitionSupportedSize @get_params).SizeMax + } + catch { + $module.FailJson("Unable to get maximum supported partition size: $($_.Exception.Message)", $_) + } + if ($size_is_maximum) { + $ansible_partition_size = $max_supported_size + } + if ( + $ansible_partition_size -ne $ansible_partition.Size -and + ($ansible_partition_size - $ansible_partition.Size -gt 1049000 -or $ansible_partition.Size - $ansible_partition_size -gt 1049000) + ) { + if ($ansible_partition.IsReadOnly) { + $module.FailJson("Unable to resize partition: Partition is read only") + } + else { + try { + $resize_params = @{ + DiskNumber = $ansible_partition.DiskNumber + PartitionNumber = $ansible_partition.PartitionNumber + Size = $ansible_partition_size + WhatIf = $module.CheckMode + } + Resize-Partition @resize_params + } + catch { + $module.FailJson("Unable to change partition size: $($_.Exception.Message)", $_) + } + $module.Result.changed = $true + } + } + elseif ($ansible_partition_size -gt $max_supported_size) { + $module.FailJson("Specified partition size exceeds size supported by the partition") + } + } + + if ($drive_letter -NotIn ("auto", $null, $ansible_partition.DriveLetter)) { + if (-not $module.CheckMode) { + try { + Set-Partition -DiskNumber $ansible_partition.DiskNumber -PartitionNumber $ansible_partition.PartitionNumber -NewDriveLetter $drive_letter + } + catch { + $module.FailJson("Unable to change drive letter: $($_.Exception.Message)", $_) + } + } + $module.Result.changed = $true + } + } +} +else { + if ($state -eq "present") { + if ($null -eq $disk_number) { + $module.FailJson("Missing required parameter: disk_number") + } + if ($null -eq $ansible_partition_size -and -not $size_is_maximum) { + $module.FailJson("Missing required parameter: partition_size") + } + if (-not $size_is_maximum) { + try { + $max_supported_size = (Get-Disk -Number $disk_number).LargestFreeExtent + } + catch { + $module.FailJson("Unable to get maximum size supported by disk: $($_.Exception.Message)", $_) + } + + if ($ansible_partition_size -gt $max_supported_size) { + $module.FailJson("Partition size is not supported by disk. Use partition_size: -1 to get maximum size") + } + } + else { + $ansible_partition_size = (Get-Disk -Number $disk_number).LargestFreeExtent + } + + $supp_part_type = (Get-Disk -Number $disk_number).PartitionStyle + if ($null -ne $mbr_type) { + if ($supp_part_type -eq "MBR" -and $mbr_styles.ContainsKey($mbr_type)) { + $partition_style = $mbr_styles.$mbr_type + } + else { + $module.FailJson("Incorrect partition style specified") + } + } + if ($null -ne $gpt_type) { + if ($supp_part_type -eq "GPT" -and $gpt_styles.ContainsKey($gpt_type)) { + $partition_style = $gpt_styles.$gpt_type + } + else { + $module.FailJson("Incorrect partition style specified") + } + } + + if (-not $module.CheckMode) { + $new_params = @{ + DiskNumber = $disk_number + Letter = $drive_letter + Size = $ansible_partition_size + MbrType = $mbr_type + GptType = $gpt_type + Style = $partition_style + } + $ansible_partition = New-AnsiblePartition @new_params + } + $module.Result.changed = $true + } +} + +if ($state -eq "present" -and $ansible_partition) { + if ($offline -NotIn ($null, $ansible_partition.IsOffline)) { + if (-not $module.CheckMode) { + try { + Set-Partition -DiskNumber $ansible_partition.DiskNumber -PartitionNumber $ansible_partition.PartitionNumber -IsOffline $offline + } + catch { + $module.FailJson("Error setting partition offline: $($_.Exception.Message)", $_) + } + } + $module.Result.changed = $true + } + + if ( + $hidden -NotIn ($null, $ansible_partition.IsHidden) -or + $read_only -NotIn ($null, $ansible_partition.IsReadOnly) -or + $active -NotIn ($null, $ansible_partition.IsActive) + ) { + if (-not $module.CheckMode) { + Set-AnsiblePartitionState -hidden $hidden -read_only $read_only -active $active -partition $ansible_partition + } + $module.Result.changed = $true + } +} + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_partition.py b/ansible_collections/community/windows/plugins/modules/win_partition.py new file mode 100644 index 000000000..5f8a46f6b --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_partition.py @@ -0,0 +1,110 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Varun Chopra (@chopraaa) <v@chopraaa.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_partition +short_description: Creates, changes and removes partitions on Windows Server +description: + - The M(community.windows.win_partition) module can create, modify or delete a partition on a disk +options: + state: + description: + - Used to specify the state of the partition. Use C(absent) to specify if a partition should be removed + and C(present) to specify if the partition should be created or updated. + type: str + choices: [ absent, present] + default: present + drive_letter: + description: + - Used for accessing partitions if I(disk_number) and I(partition_number) are not provided. + - Use C(auto) for automatically assigning a drive letter, or a letter A-Z for manually assigning a drive letter to a new partition. + If not specified, no drive letter is assigned when creating a new partition. + type: str + disk_number: + description: + - Disk number is mandatory for creating new partitions. + - A combination of I(disk_number) and I(partition_number) can be used to specify the partition instead of I(drive_letter) if required. + type: int + partition_number: + description: + - Used in conjunction with I(disk_number) to uniquely identify a partition. + type: int + partition_size: + description: + - Specify size of the partition in B, KB, KiB, MB, MiB, GB, GiB, TB or TiB. Use -1 to specify maximum supported size. + - Partition size is mandatory for creating a new partition but not for updating or deleting a partition. + - The decimal SI prefixes kilo, mega, giga, tera, etc., are powers of 10^3 = 1000. The binary prefixes kibi, mebi, gibi, tebi, etc. + respectively refer to the corresponding power of 2^10 = 1024. + Thus, a gigabyte (GB) is 1000000000 (1000^3) bytes while 1 gibibyte (GiB) is 1073741824 (1024^3) bytes. + type: str + read_only: + description: + - Make the partition read only, restricting changes from being made to the partition. + type: bool + active: + description: + - Specifies if the partition is active and can be used to start the system. This property is only valid when the disk's partition style is MBR. + type: bool + hidden: + description: + - Hides the target partition, making it undetectable by the mount manager. + type: bool + offline: + description: + - Sets the partition offline. + - Adding a mount point (such as a drive letter) will cause the partition to go online again. + type: bool + required: no + mbr_type: + description: + - Specify the partition's MBR type if the disk's partition style is MBR. + - This only applies to new partitions. + - This does not relate to the partitions file system formatting. + type: str + choices: [ fat12, fat16, extended, huge, ifs, fat32 ] + gpt_type: + description: + - Specify the partition's GPT type if the disk's partition style is GPT. + - This only applies to new partitions. + - This does not relate to the partitions file system formatting. + type: str + choices: [ system_partition, microsoft_reserved, basic_data, microsoft_recovery ] + +notes: + - A minimum Operating System Version of 6.2 is required to use this module. To check if your OS is compatible, see + U(https://docs.microsoft.com/en-us/windows/desktop/sysinfo/operating-system-version). + - This module cannot be used for removing the drive letter associated with a partition, initializing a disk or, file system formatting. + - Idempotence works only if you're specifying a drive letter or other unique attributes such as a combination of disk number and partition number. + - For more information, see U(https://msdn.microsoft.com/en-us/library/windows/desktop/hh830524.aspx). +author: + - Varun Chopra (@chopraaa) <v@chopraaa.com> +''' + +EXAMPLES = r''' +- name: Create a partition with drive letter D and size 5 GiB + community.windows.win_partition: + drive_letter: D + partition_size: 5 GiB + disk_number: 1 + +- name: Resize previously created partition to it's maximum size and change it's drive letter to E + community.windows.win_partition: + drive_letter: E + partition_size: -1 + partition_number: 1 + disk_number: 1 + +- name: Delete partition + community.windows.win_partition: + disk_number: 1 + partition_number: 1 + state: absent +''' + +RETURN = r''' +# +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_pester.ps1 b/ansible_collections/community/windows/plugins/modules/win_pester.ps1 new file mode 100644 index 000000000..5ffcf5b7e --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_pester.ps1 @@ -0,0 +1,125 @@ +#!powershell + +# Copyright: (c) 2017, Erwan Quelin (@equelin) <erwan.quelin@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic + +$spec = @{ + options = @{ + output_file = @{ type = "str" } + output_format = @{ type = "str"; default = "NunitXML" } + path = @{ type = "str"; required = $true } + tags = @{ type = "list"; elements = "str" } + test_parameters = @{ type = "dict" } + version = @{ type = "str"; aliases = @(, "minimum_version") } + } + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$output_file = $module.Params.output_file +$output_format = $module.Params.output_format +$path = $module.Params.path +$tags = $module.Params.tags +$test_parameters = $module.Params.test_parameters +$version = $module.Params.version + +Try { + $version = [version]$version +} +Catch { + $module.FailJson("Value '$version' for parameter 'minimum_version' is not a valid version format") +} + +# Make sure path is a real path +Try { + $path = $path.TrimEnd("\") + $path = (Get-item -LiteralPath $path).FullName +} +Catch { + $module.FailJson("Cannot find file or directory: '$path' as it does not exist") +} + +# Import Pester module if available +$Pester = 'Pester' + +If (-not (Get-Module -Name $Pester -ErrorAction SilentlyContinue)) { + If (Get-Module -Name $Pester -ListAvailable -ErrorAction SilentlyContinue) { + Import-Module $Pester + } + else { + $msg = -join @( + "Cannot find module: $Pester. Check if pester is installed, and if it is not, " + "install using win_psmodule or chocolatey.chocolatey.win_chocolatey." + ) + $module.FailJson($msg) + } +} + +# Add actual pester's module version in the ansible's result variable +$Pester_version = (Get-Module -Name $Pester).Version.ToString() +$module.Result.pester_version = $Pester_version + +# Test if the Pester module is available with a version greater or equal than the one specified in the $version parameter +If ((-not (Get-Module -Name $Pester -ErrorAction SilentlyContinue | Where-Object { $_.Version -ge $version })) -and ($version)) { + $module.FailJson("$Pester version is not greater or equal to $version") +} + +#Prepare Invoke-Pester parameters depending of the Pester's version. +#Invoke-Pester output deactivation behave differently depending on the Pester's version +If ($module.Result.pester_version -ge "4.0.0") { + $Parameters = @{ + "show" = "none" + "PassThru" = $True + } +} +else { + $Parameters = @{ + "quiet" = $True + "PassThru" = $True + } +} + +if ($tags.count) { + $Parameters.Tag = $tags +} + +if ($output_file) { + $Parameters.OutputFile = $output_file + $Parameters.OutputFormat = $output_format +} + +# Run Pester tests +If (Test-Path -LiteralPath $path -PathType Leaf) { + $test_parameters_check_mode_msg = '' + if ($test_parameters.keys.count) { + $Parameters.Script = @{Path = $Path ; Parameters = $test_parameters } + $test_parameters_check_mode_msg = " with $($test_parameters.keys -join ',') parameters" + } + else { + $Parameters.Script = $Path + } + + if ($module.CheckMode) { + $module.Result.output = "Run pester test in the file: $path$test_parameters_check_mode_msg" + } + else { + $module.Result.output = Invoke-Pester @Parameters + } +} +else { + $Parameters.Script = $path + + if ($module.CheckMode) { + $module.Result.output = "Run Pester test(s): $path" + } + else { + $module.Result.output = Invoke-Pester @Parameters + } +} + +$module.Result.changed = $true + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_pester.py b/ansible_collections/community/windows/plugins/modules/win_pester.py new file mode 100644 index 000000000..83cc74fab --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_pester.py @@ -0,0 +1,104 @@ +#!/usr/bin/python +# -*- 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) + +DOCUMENTATION = r''' +--- +module: win_pester +short_description: Run Pester tests on Windows hosts +description: + - Run Pester tests on Windows hosts. + - Test files have to be available on the remote host. +requirements: + - Pester +options: + path: + description: + - Path to a pester test file or a folder where tests can be found. + - If the path is a folder, the module will consider all ps1 files as Pester tests. + type: str + required: true + tags: + description: + - Runs only tests in Describe blocks with specified Tags values. + - Accepts multiple comma separated tags. + type: list + elements: str + output_file: + description: + - Generates an output test report. + type: str + output_format: + description: + - Format of the test report to be generated. + - This parameter is to be used with output_file option. + type: str + default: NunitXML + test_parameters: + description: + - Allows to specify parameters to the test script. + type: dict + version: + description: + - Minimum version of the pester module that has to be available on the remote host. + type: str + aliases: + - minimum_version +author: + - Erwan Quelin (@equelin) + - Prasoon Karunan V (@prasoonkarunan) +''' + +EXAMPLES = r''' +- name: Get facts + ansible.windows.setup: + +- name: Add Pester module + action: + module_name: "{{ 'community.windows.win_psmodule' if ansible_powershell_version >= 5 else 'chocolatey.chocolatey.win_chocolatey' }}" + name: Pester + state: present + +- name: Run the pester test provided in the path parameter. + community.windows.win_pester: + path: C:\Pester + +- name: Run the pester tests only for the tags specified. + community.windows.win_pester: + path: C:\Pester\TestScript.tests + tags: CI,UnitTests + +# Run pesters tests files that are present in the specified folder +# ensure that the pester module version available is greater or equal to the version parameter. +- name: Run the pester test present in a folder and check the Pester module version. + community.windows.win_pester: + path: C:\Pester\test01.test.ps1 + version: 4.1.0 + +- name: Run the pester test present in a folder with given script parameters. + community.windows.win_pester: + path: C:\Pester\test04.test.ps1 + test_parameters: + Process: lsass + Service: bits + +- name: Run the pester test present in a folder and generate NunitXML test result.. + community.windows.win_pester: + path: C:\Pester\test04.test.ps1 + output_file: c:\Pester\resullt\testresult.xml +''' + +RETURN = r''' +pester_version: + description: Version of the pester module found on the remote host. + returned: always + type: str + sample: 4.3.1 +output: + description: Results of the Pester tests. + returned: success + type: list + sample: false +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_power_plan.ps1 b/ansible_collections/community/windows/plugins/modules/win_power_plan.ps1 new file mode 100644 index 000000000..0ebad6302 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_power_plan.ps1 @@ -0,0 +1,231 @@ +#!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.AddType + +$spec = @{ + options = @{ + name = @{ type = "str"; } + guid = @{ type = "str"; } + } + required_one_of = @( + , @('name', 'guid') + ) + supports_check_mode = $true +} +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$name = $module.Params.name +$guid = $module.Params.guid +$module.Result.power_plan_name = $name +$module.Result.power_plan_enabled = $null +$module.Result.all_available_plans = $null + +Add-CSharpType -References @" +using System; +using System.Runtime.InteropServices; + +namespace Ansible.WinPowerPlan +{ + public enum AccessFlags : uint + { + AccessScheme = 16, + AccessSubgroup = 17, + AccessIndividualSetting = 18 + } + + public class NativeMethods + { + [DllImport("Kernel32.dll", SetLastError = true)] + public static extern IntPtr LocalFree( + IntPtr hMen); + + [DllImport("PowrProf.dll")] + public static extern UInt32 PowerEnumerate( + IntPtr RootPowerKey, + IntPtr SchemeGuid, + IntPtr SubGroupOfPowerSettingsGuid, + AccessFlags AccessFlags, + UInt32 Index, + IntPtr Buffer, + ref UInt32 BufferSize); + + [DllImport("PowrProf.dll")] + public static extern UInt32 PowerGetActiveScheme( + IntPtr UserRootPowerKey, + out IntPtr ActivePolicyGuid); + + [DllImport("PowrProf.dll")] + public static extern UInt32 PowerReadFriendlyName( + IntPtr RootPowerKey, + Guid SchemeGuid, + IntPtr SubGroupOfPowerSettingsGuid, + IntPtr PowerSettingGuid, + IntPtr Buffer, + ref UInt32 BufferSize); + + [DllImport("PowrProf.dll")] + public static extern UInt32 PowerSetActiveScheme( + IntPtr UserRootPowerKey, + Guid SchemeGuid); + } +} +"@ + +Function Get-LastWin32ErrorMessage { + param([Int]$ErrorCode) + $exp = New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList $ErrorCode + $error_msg = "{0} - (Win32 Error Code {1} - 0x{1:X8})" -f $exp.Message, $ErrorCode + return $error_msg +} + +Function Get-PlanName { + param([Guid]$Plan) + + $buffer_size = 0 + $buffer = [IntPtr]::Zero + [Ansible.WinPowerPlan.NativeMethods]::PowerReadFriendlyName([IntPtr]::Zero, $Plan, [IntPtr]::Zero, [IntPtr]::Zero, + $buffer, [ref]$buffer_size) > $null + + $buffer = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($buffer_size) + try { + $res = [Ansible.WinPowerPlan.NativeMethods]::PowerReadFriendlyName([IntPtr]::Zero, $Plan, [IntPtr]::Zero, + [IntPtr]::Zero, $buffer, [ref]$buffer_size) + + if ($res -ne 0) { + $err_msg = Get-LastWin32ErrorMessage -ErrorCode $res + $module.FailJson("Failed to get name for power scheme $Plan - $err_msg") + } + + return [System.Runtime.InteropServices.Marshal]::PtrToStringUni($buffer) + } + finally { + [System.Runtime.InteropServices.Marshal]::FreeHGlobal($buffer) + } +} + +Function Get-PowerPlan { + $plans = @{} + + $i = 0 + while ($true) { + $buffer_size = 0 + $buffer = [IntPtr]::Zero + $res = [Ansible.WinPowerPlan.NativeMethods]::PowerEnumerate([IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero, + [Ansible.WinPowerPlan.AccessFlags]::AccessScheme, $i, $buffer, [ref]$buffer_size) + + if ($res -eq 259) { + # 259 == ERROR_NO_MORE_ITEMS, there are no more power plans to enumerate + break + } + elseif ($res -notin @(0, 234)) { + # 0 == ERROR_SUCCESS and 234 == ERROR_MORE_DATA + $err_msg = Get-LastWin32ErrorMessage -ErrorCode $res + $module.FailJson("Failed to get buffer size on local power schemes at index $i - $err_msg") + } + + $buffer = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($buffer_size) + try { + $res = [Ansible.WinPowerPlan.NativeMethods]::PowerEnumerate([IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero, + [Ansible.WinPowerPlan.AccessFlags]::AccessScheme, $i, $buffer, [ref]$buffer_size) + + if ($res -eq 259) { + # Server 2008 does not return 259 in the first call above so we do an additional check here + break + } + elseif ($res -notin @(0, 234, 259)) { + $err_msg = Get-LastWin32ErrorMessage -ErrorCode $res + $module.FailJson("Failed to enumerate local power schemes at index $i - $err_msg") + } + $scheme_guid = [System.Runtime.InteropServices.Marshal]::PtrToStructure($buffer, [Type][Guid]) + } + finally { + [System.Runtime.InteropServices.Marshal]::FreeHGlobal($buffer) + } + $scheme_name = Get-PlanName -Plan $scheme_guid + $plans.$scheme_name = $scheme_guid + + $i += 1 + } + + return $plans +} + +Function Get-ActivePowerPlan { + $buffer = [IntPtr]::Zero + $res = [Ansible.WinPowerPlan.NativeMethods]::PowerGetActiveScheme([IntPtr]::Zero, [ref]$buffer) + if ($res -ne 0) { + $err_msg = Get-LastWin32ErrorMessage -ErrorCode $res + $module.FailJson("Failed to get the active power plan - $err_msg") + } + + try { + $active_guid = [System.Runtime.InteropServices.Marshal]::PtrToStructure($buffer, [Type][Guid]) + } + finally { + [Ansible.WinPowerPlan.NativeMethods]::LocalFree($buffer) > $null + } + + return $active_guid +} + +Function Set-ActivePowerPlan { + [CmdletBinding(SupportsShouldProcess = $true)] + param([Guid]$Plan) + + $res = 0 + if ($PSCmdlet.ShouldProcess($Plan, "Set Power Plan")) { + $res = [Ansible.WinPowerPlan.NativeMethods]::PowerSetActiveScheme([IntPtr]::Zero, $Plan) + } + + if ($res -ne 0) { + $err_msg = Get-LastWin32ErrorMessage -ErrorCode $res + $module.FailJson("Failed to set the active power plan to $Plan - $err_msg") + } +} + +# Get all local power plans and the current active plan +$plans = Get-PowerPlan +$active_plan = Get-ActivePowerPlan +$module.Result.all_available_plans = @{} +foreach ($plan_info in $plans.GetEnumerator()) { + $module.Result.all_available_plans.($plan_info.Key) = $plan_info.Value -eq $active_plan +} + +if ($null -ne $name -and $name -notin $plans.Keys) { + $module.FailJson("Defined power_plan: ($name) is not available") +} +if ($null -ne $guid -and $guid -notin $plans.Values) { + $module.FailJson("Defined power_plan: ($guid) is not available") +} +if ($null -ne $name) { + $plan_guid = $plans.$name + $is_active = $active_plan -eq $plans.$name +} +if ($null -ne $guid) { + $plan_guid = $guid + $name = $plans.GetEnumerator() | ForEach-Object { + $name = $_.Key + if ($Plans.Item($name) -eq $plan_guid) { + $name + } + } + $is_active = $active_plan -eq $plans.$name +} + +$module.Result.power_plan_enabled = $is_active + +if (-not $is_active) { + Set-ActivePowerPlan -Plan $plan_guid -WhatIf:$module.CheckMode + $module.Result.changed = $true + $module.Result.power_plan_enabled = $true + foreach ($plan_info in $plans.GetEnumerator()) { + $is_active = $plan_info.Value -eq $plan_guid + $module.Result.all_available_plans.($plan_info.Key) = $is_active + } +} + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_power_plan.py b/ansible_collections/community/windows/plugins/modules/win_power_plan.py new file mode 100644 index 000000000..a4d07c7a1 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_power_plan.py @@ -0,0 +1,68 @@ +#!/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) + +DOCUMENTATION = r''' +--- +module: win_power_plan +short_description: Changes the power plan of a Windows system +description: + - This module will change the power plan of a Windows system to the defined string. + - Windows defaults to C(balanced) which will cause CPU throttling. In some cases it can be preferable + to change the mode to C(high performance) to increase CPU performance. + - One of I(name) or I(guid) must be provided. +options: + name: + description: + - String value that indicates the desired power plan by name. + - The power plan must already be present on the system. + - Commonly there will be options for C(balanced) and C(high performance). + type: str + required: false + guid: + description: + - String value that indicates the desired power plan by guid. + - The power plan must already be present on the system. + - For out of box guids see U(https://docs.microsoft.com/en-us/windows/win32/power/power-policy-settings). + type: str + required: false + version_added: 1.9.0 + +author: + - Noah Sparks (@nwsparks) +''' + +EXAMPLES = r''' +- name: Change power plan to high performance + community.windows.win_power_plan: + name: high performance + +- name: Change power plan to high performance + community.windows.win_power_plan: + guid: 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c +''' + +RETURN = r''' +power_plan_name: + description: Value of the intended power plan. + returned: always + type: str + sample: balanced +power_plan_enabled: + description: State of the intended power plan. + returned: success + type: bool + sample: true +all_available_plans: + description: The name and enabled state of all power plans. + returned: always + type: dict + sample: | + { + "High performance": false, + "Balanced": true, + "Power saver": false + } +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_product_facts.ps1 b/ansible_collections/community/windows/plugins/modules/win_product_facts.ps1 new file mode 100644 index 000000000..18519af81 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_product_facts.ps1 @@ -0,0 +1,105 @@ +#!powershell + +# Copyright: (c) 2017, Dag Wieers (@dagwieers) <dag@wieers.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic + +# This modules does not accept any options +$spec = @{ + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +# First try to find the product key from ACPI +try { + $product_key = (Get-CimInstance -Class SoftwareLicensingService).OA3xOriginalProductKey +} +catch { + $product_key = $null +} + +if (-not $product_key) { + # Else try to get it from the registry instead + try { + $data = Get-ItemPropertyValue -LiteralPath "HKLM:\Software\Microsoft\Windows NT\CurrentVersion" -Name DigitalProductId + } + catch { + $data = $null + } + + # And for Windows 2008 R2 + if (-not $data) { + try { + $data = Get-ItemPropertyValue -LiteralPath "HKLM:\Software\Microsoft\Windows NT\CurrentVersion" -Name DigitalProductId4 + } + catch { + $data = $null + } + } + + if ($data) { + $product_key = $null + $isWin8 = [int]($data[66] / 6) -band 1 + $HF7 = 0xF7 + $data[66] = ($data[66] -band $HF7) -bOr (($isWin8 -band 2) * 4) + $hexdata = $data[52..66] + $chardata = "B", "C", "D", "F", "G", "H", "J", "K", "M", "P", "Q", "R", "T", "V", "W", "X", "Y", "2", "3", "4", "6", "7", "8", "9" + + # Decode base24 binary data + for ($i = 24; $i -ge 0; $i--) { + $k = 0 + for ($j = 14; $j -ge 0; $j--) { + $k = $k * 256 -bxor $hexdata[$j] + $hexdata[$j] = [math]::truncate($k / 24) + $k = $k % 24 + } + $product_key_output = $chardata[$k] + $product_key_output + $last = $k + } + + $product_key_tmp1 = $product_key_output.SubString(1, $last) + $product_key_tmp2 = $product_key_output.SubString(1, $product_key_output.Length - 1) + if ($last -eq 0) { + $product_key_output = "N" + $product_key_tmp2 + } + else { + $product_key_output = $product_key_tmp2.Insert($product_key_tmp2.IndexOf($product_key_tmp1) + $product_key_tmp1.Length, "N") + } + $num = 0 + $product_key_split = @() + for ($i = 0; $i -le 4; $i++) { + $product_key_split += $product_key_output.SubString($num, 5) + $num += 5 + } + $product_key = $product_key_split -join "-" + } +} + +# Retrieve license information +$license_info = Get-CimInstance SoftwareLicensingProduct | Where-Object PartialProductKey + +$winlicense_status = switch ($license_info.LicenseStatus) { + 0 { "Unlicensed" } + 1 { "Licensed" } + 2 { "OOBGrace" } + 3 { "OOTGrace" } + 4 { "NonGenuineGrace" } + 5 { "Notification" } + 6 { "ExtendedGrace" } + default { $null } +} + +$winlicense_edition = $license_info.Name +$winlicense_channel = $license_info.ProductKeyChannel + +$module.Result.ansible_facts = @{ + ansible_os_product_id = (Get-CimInstance Win32_OperatingSystem).SerialNumber + ansible_os_product_key = $product_key + ansible_os_license_edition = $winlicense_edition + ansible_os_license_channel = $winlicense_channel + ansible_os_license_status = $winlicense_status +} + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_product_facts.py b/ansible_collections/community/windows/plugins/modules/win_product_facts.py new file mode 100644 index 000000000..5353ce556 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_product_facts.py @@ -0,0 +1,61 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2017, Dag Wieers (@dagwieers) <dag@wieers.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_product_facts +short_description: Provides Windows product and license information +description: +- Provides Windows product and license information. +author: +- Dag Wieers (@dagwieers) +''' + +EXAMPLES = r''' +- name: Get product id and product key + community.windows.win_product_facts: + +- name: Display Windows edition + debug: + var: ansible_os_license_edition + +- name: Display Windows license status + debug: + var: ansible_os_license_status +''' + +RETURN = r''' +ansible_facts: + description: Dictionary containing all the detailed information about the Windows product and license. + returned: always + type: complex + contains: + ansible_os_license_channel: + description: The Windows license channel. + returned: always + type: str + sample: Volume:MAK + ansible_os_license_edition: + description: The Windows license edition. + returned: always + type: str + sample: Windows(R) ServerStandard edition + ansible_os_license_status: + description: The Windows license status. + returned: always + type: str + sample: Licensed + ansible_os_product_id: + description: The Windows product ID. + returned: always + type: str + sample: 00326-10000-00000-AA698 + ansible_os_product_key: + description: The Windows product key. + returned: always + type: str + sample: T49TD-6VFBW-VV7HY-B2PXY-MY47H +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_psexec.ps1 b/ansible_collections/community/windows/plugins/modules/win_psexec.ps1 new file mode 100644 index 000000000..76c5ff8ae --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_psexec.ps1 @@ -0,0 +1,161 @@ +#!powershell + +# Copyright: (c) 2017, Dag Wieers (@dagwieers) <dag@wieers.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic +#Requires -Module Ansible.ModuleUtils.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'; elements = 'str' } + 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-Object -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 + +# Replace password with *PASSWORD_REPLACED* to avoid disclosing sensitive data +$toLog = $argument_string +if ($password) { + $maskedPassword = Argv-ToString $password + $toLog = $toLog.Replace($maskedPassword, "*PASSWORD_REPLACED*") +} + +$module.Result.psexec_command = $toLog + +$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/ansible_collections/community/windows/plugins/modules/win_psexec.py b/ansible_collections/community/windows/plugins/modules/win_psexec.py new file mode 100644 index 000000000..44d37654e --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_psexec.py @@ -0,0 +1,167 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: 2017, Dag Wieers (@dagwieers) <dag@wieers.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_psexec +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 + elements: str + 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 + 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 + 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 ] + type: str + 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: community.windows.psexec +- module: ansible.builtin.raw +- module: ansible.windows.win_command +- module: ansible.windows.win_shell +author: +- Dag Wieers (@dagwieers) +''' + +EXAMPLES = r''' +- name: Test the PsExec connection to the local system (target node) with your user + community.windows.win_psexec: + command: whoami.exe + +- name: Run regedit.exe locally (on target node) as SYSTEM and interactively + community.windows.win_psexec: + command: regedit.exe + interactive: yes + system: yes + +- name: Run the setup.exe installer on multiple servers using the Domain Administrator + community.windows.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\ + community.windows.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/ansible_collections/community/windows/plugins/modules/win_psmodule.ps1 b/ansible_collections/community/windows/plugins/modules/win_psmodule.ps1 new file mode 100644 index 000000000..f755e8dda --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_psmodule.ps1 @@ -0,0 +1,574 @@ +#!powershell + +# Copyright: (c) 2018, Wojciech Sciesinski <wojciech[at]sciesinski[dot]net> +# Copyright: (c) 2017, Daniele Lazzari <lazzari@mailup.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#Requires -Module Ansible.ModuleUtils.Legacy + +# win_psmodule (Windows PowerShell modules Additions/Removals/Updates) + +$params = Parse-Args -arguments $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 "str" -failifempty $true +$required_version = Get-AnsibleParam -obj $params -name "required_version" -type "str" +$minimum_version = Get-AnsibleParam -obj $params -name "minimum_version" -type "str" +$maximum_version = Get-AnsibleParam -obj $params -name "maximum_version" -type "str" +$repo = Get-AnsibleParam -obj $params -name "repository" -type "str" +$repo_user = Get-AnsibleParam -obj $params -name "username" -type "str" +$repo_pass = Get-AnsibleParam -obj $params -name "password" -type "str" +$url = Get-AnsibleParam -obj $params -name "url" -type str +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present", "absent", "latest" +$allow_clobber = Get-AnsibleParam -obj $params -name "allow_clobber" -type "bool" -default $false +$skip_publisher_check = Get-AnsibleParam -obj $params -name "skip_publisher_check" -type "bool" -default $false +$allow_prerelease = Get-AnsibleParam -obj $params -name "allow_prerelease" -type "bool" -default $false +$accept_license = Get-AnsibleParam -obj $params -name "accept_license" -type "bool" -default $false +$force = Get-AnsibleParam -obj $params -name "force" -type "bool" -default $false + +$result = @{changed = $false + output = "" + nuget_changed = $false + repository_changed = $false +} + + +# Enable TLS1.1/TLS1.2 if they're available but disabled (eg. .NET 4.5) +$security_protocols = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::SystemDefault +if ([System.Net.SecurityProtocolType].GetMember("Tls11").Count -gt 0) { + $security_protocols = $security_protocols -bor [System.Net.SecurityProtocolType]::Tls11 +} +if ([System.Net.SecurityProtocolType].GetMember("Tls12").Count -gt 0) { + $security_protocols = $security_protocols -bor [System.Net.SecurityProtocolType]::Tls12 +} +[System.Net.ServicePointManager]::SecurityProtocol = $security_protocols + + +Function Install-NugetProvider { + Param( + [Bool]$CheckMode + ) + $PackageProvider = Get-PackageProvider -ListAvailable | Where-Object { ($_.name -eq 'Nuget') -and ($_.version -ge "2.8.5.201") } + if (-not($PackageProvider)) { + try { + Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -WhatIf:$CheckMode | Out-Null + $result.changed = $true + $result.nuget_changed = $true + } + catch [ System.Exception ] { + $ErrorMessage = "Problems adding package provider: $($_.Exception.Message)" + Fail-Json $result $ErrorMessage + } + } +} + +Function Install-PrereqModule { + Param( + [Switch]$TestInstallationOnly, + [bool]$AllowClobber, + [Bool]$CheckMode, + [bool]$AcceptLicense, + [string]$Repository + ) + + # Those are minimum required versions of modules. + $PrereqModules = @{ + PackageManagement = '1.1.7' + PowerShellGet = '1.6.0' + } + + $ExistingPrereqModule = Get-Module -Name @($PrereqModules.Keys) -ListAvailable | + Where-Object { $_.Version -ge $PreReqModules[$_.Name] } | + Select-Object -ExpandProperty Name -Unique + $toInstall = $PrereqModules.Keys | + Where-Object { $_ -notin $ExistingPrereqModule } | + Select-Object @{N = 'Name'; E = { $_ } }, @{N = 'Version'; E = { $PrereqModules[$_] } } + + if ($TestInstallationOnly) { + -not [bool]$toInstall + } + elseif ($toInstall) { + $result.changed = $true + if ($CheckMode) { + $result.output = "Skipped check mode run on win_psmodule as the pre-reqs need upgrading" + Exit-Json $result + } + + # Need to run this in another process as importing PackageManagement at + # the older version is irreversible (you cannot load the same dll at + # different versions). Start-Job runs in a new process so it can safely + # use the older version to install our pre-reqs. + # https://github.com/ansible-collections/community.windows/issues/487 + $job = Start-Job -ScriptBlock { + $ErrorActionPreference = 'Stop' + + $Repository = $using:Repository + + foreach ($info in $using:ToInstall) { + $install_params = @{ + Name = $info.Name + MinimumVersion = $info.Version + Force = $true + } + $installCmd = Get-Command -Name Install-Module + if ($installCmd.Parameters.ContainsKey('SkipPublisherCheck')) { + $install_params.SkipPublisherCheck = $true + } + if ($installCmd.Parameters.ContainsKey('AllowClobber')) { + $install_params.AllowClobber = $using:AllowClobber + } + if ($installCmd.Parameters.ContainsKey('AcceptLicense')) { + $install_params.AcceptLicense = $using:AcceptLicense + } + if ($Repository) { + $install_params.Repository = $Repository + } + + Install-Module @install_params > $null + } + } + + try { + $null = $job | Receive-Job -AutoRemoveJob -Wait -ErrorAction Stop + } + catch { + $ErrorMessage = "Problems adding a prerequisite module PackageManagement or PowerShellGet: $($_.Exception.Message)" + $result.exception = ($_ | Out-String) + "`r`n" + $_.ScriptStackTrace + Fail-Json $result $ErrorMessage + } + } +} + +Function Get-PsModule { + Param( + [Parameter(Mandatory = $true)] + [String]$Name, + [String]$RequiredVersion, + [String]$MinimumVersion, + [String]$MaximumVersion + ) + + $ExistingModule = @{ + Exists = $false + Version = "" + } + + $ExistingModules = Get-Module -ListAvailable | Where-Object { ($_.name -eq $Name) } + $ExistingModulesCount = $($ExistingModules | Measure-Object).Count + + if ( $ExistingModulesCount -gt 0 ) { + + $ExistingModules | Add-Member -MemberType ScriptProperty -Name FullVersion -Value { + if ( $null -ne ( $this.PrivateData ) ) { + [String]"$($this.Version)-$(($this | Select-Object -ExpandProperty PrivateData).PSData.Prerelease)".TrimEnd('-') + } + else { + [String]"$($this.Version)" + } + } + + if ( -not ($RequiredVersion -or + $MinimumVersion -or + $MaximumVersion) ) { + + $ReturnedModule = $ExistingModules | Select-Object -First 1 + } + elseif ( $RequiredVersion ) { + $ReturnedModule = $ExistingModules | Where-Object -FilterScript { $_.FullVersion -eq $RequiredVersion } + } + elseif ( $MinimumVersion -and $MaximumVersion ) { + $ReturnedModule = $ExistingModules | + Where-Object -FilterScript { $MinimumVersion -le $_.Version -and $MaximumVersion -ge $_.Version } | + Select-Object -First 1 + } + elseif ( $MinimumVersion ) { + $ReturnedModule = $ExistingModules | Where-Object -FilterScript { $MinimumVersion -le $_.Version } | Select-Object -First 1 + } + elseif ( $MaximumVersion ) { + $ReturnedModule = $ExistingModules | Where-Object -FilterScript { $MaximumVersion -ge $_.Version } | Select-Object -First 1 + } + } + + $ReturnedModuleCount = ($ReturnedModule | Measure-Object).Count + + if ( $ReturnedModuleCount -eq 1 ) { + $ExistingModule.Exists = $true + $ExistingModule.Version = $ReturnedModule.FullVersion + } + + $ExistingModule +} + +Function Add-DefinedParameter { + Param ( + [Parameter(Mandatory = $true)] + [Hashtable]$Hashtable, + [Parameter(Mandatory = $true)] + [String[]]$ParametersNames + ) + + ForEach ($ParameterName in $ParametersNames) { + $ParameterVariable = Get-Variable -Name $ParameterName -ErrorAction SilentlyContinue + if ( $ParameterVariable.Value -and $Hashtable.Keys -notcontains $ParameterName ) { + $Hashtable.Add($ParameterName, $ParameterVariable.Value) + } + } + + $Hashtable +} + +Function Install-PsModule { + Param( + [Parameter(Mandatory = $true)] + [String]$Name, + [String]$RequiredVersion, + [String]$MinimumVersion, + [String]$MaximumVersion, + [String]$Repository, + [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty, + [Bool]$AllowClobber, + [Bool]$SkipPublisherCheck, + [Bool]$AllowPrerelease, + [Bool]$CheckMode, + [Bool]$AcceptLicense, + [Bool]$Force + ) + + $getParams = @{ + Name = $Name + RequiredVersion = $RequiredVersion + MinimumVersion = $MinimumVersion + MaximumVersion = $MaximumVersion + } + $ExistingModuleBefore = Get-PsModule @getParams + + if ( -not $ExistingModuleBefore.Exists ) { + try { + # Install NuGet provider if needed. + Install-NugetProvider -CheckMode $CheckMode + + $ht = @{ + Name = $Name + } + + [String[]]$ParametersNames = @("RequiredVersion", "MinimumVersion", "MaximumVersion", "AllowPrerelease", + "Repository", "Credential") + $ht = Add-DefinedParameter -Hashtable $ht -ParametersNames $ParametersNames + + # When module require License Acceptance, `-Force` is mandatory to skip interactive prompt + if ((Find-Module @ht).AdditionalMetadata.requireLicenseAcceptance) { + $ht["Force"] = $true + } + else { + $ht["Force"] = $Force + } + + $ht = $ht + @{ + WhatIf = $CheckMode + AcceptLicense = $AcceptLicense + } + + [String[]]$ParametersNames = @("AllowClobber", "SkipPublisherCheck") + $ht = Add-DefinedParameter -Hashtable $ht -ParametersNames $ParametersNames + + Install-Module @ht -ErrorVariable ErrorDetails | Out-Null + + $result.changed = $true + $result.output = "Module $($Name) installed" + } + catch [ System.Exception ] { + $ErrorMessage = "Problems installing $($Name) module: $($_.Exception.Message)" + Fail-Json $result $ErrorMessage + } + } + else { + $result.output = "Module $($Name) already present" + } +} + +Function Remove-PsModule { + Param( + [Parameter(Mandatory = $true)] + [String]$Name, + [String]$RequiredVersion, + [String]$MinimumVersion, + [String]$MaximumVersion, + [Bool]$CheckMode + ) + # If module is present, uninstalls it. + if (Get-Module -ListAvailable | Where-Object { $_.name -eq $Name }) { + try { + $ht = @{ + Name = $Name + Confirm = $false + Force = $true + } + + $ExistingModuleBefore = Get-PsModule -Name $Name -RequiredVersion $RequiredVersion -MinimumVersion $MinimumVersion -MaximumVersion $MaximumVersion + + [String[]]$ParametersNames = @("RequiredVersion", "MinimumVersion", "MaximumVersion") + + $ht = Add-DefinedParameter -Hashtable $ht -ParametersNames $ParametersNames + + if ( -not ( $RequiredVersion -or $MinimumVersion -or $MaximumVersion ) ) { + $ht.Add("AllVersions", $true) + } + + if ( $ExistingModuleBefore.Exists) { + # The Force parameter overwrite the WhatIf parameter + if ( -not $CheckMode ) { + Uninstall-Module @ht -ErrorVariable ErrorDetails | Out-Null + } + $result.changed = $true + $result.output = "Module $($Name) removed" + } + } + catch [ System.Exception ] { + $ErrorMessage = "Problems uninstalling $($Name) module: $($_.Exception.Message)" + Fail-Json $result $ErrorMessage + } + } + else { + $result.output = "Module $($Name) removed" + } +} + +Function Find-LatestPsModule { + Param( + [Parameter(Mandatory = $true)] + [String]$Name, + [String]$Repository, + [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty, + [Bool]$AllowPrerelease, + [Bool]$CheckMode + ) + + try { + $ht = @{ + Name = $Name + } + + [String[]]$ParametersNames = @("AllowPrerelease", "Repository", "Credential") + + $ht = Add-DefinedParameter -Hashtable $ht -ParametersNames $ParametersNames + + $LatestModule = Find-Module @ht + $LatestModuleVersion = $LatestModule.Version + } + catch [ System.Exception ] { + $ErrorMessage = "Cant find the module $($Name): $($_.Exception.Message)" + Fail-Json $result $ErrorMessage + } + + $LatestModuleVersion +} + +Function Install-Repository { + Param( + [Parameter(Mandatory = $true)] + [string]$Name, + [Parameter(Mandatory = $true)] + [string]$Url, + [bool]$CheckMode + ) + # Legacy doesn't natively support deprecate by date, need to do this manually until we use Ansible.Basic + if (-not $result.ContainsKey('deprecations')) { + $result.deprecations = @() + } + $msg = -join @( + "Adding a repo with this module is deprecated, the repository parameter should only be used to select a repo. " + "Use community.windows.win_psrepository to manage repos" + ) + $result.deprecations += @{ + msg = $msg + date = "2021-07-01" + collection_name = "community.windows" + } + # Install NuGet provider if needed. + Install-NugetProvider -CheckMode $CheckMode + + $Repos = (Get-PSRepository).SourceLocation + + # If repository isn't already present, try to register it as trusted. + if ($Repos -notcontains $Url) { + try { + if ( -not ($CheckMode) ) { + Register-PSRepository -Name $Name -SourceLocation $Url -InstallationPolicy Trusted -ErrorAction Stop + } + $result.changed = $true + $result.repository_changed = $true + } + catch { + $ErrorMessage = "Problems registering $($Name) repository: $($_.Exception.Message)" + Fail-Json $result $ErrorMessage + } + } +} + +Function Remove-Repository { + Param( + [Parameter(Mandatory = $true)] + [string]$Name, + [bool]$CheckMode + ) + # Legacy doesn't natively support deprecate by date, need to do this manually until we use Ansible.Basic + if (-not $result.ContainsKey('deprecations')) { + $result.deprecations = @() + } + $result.deprecations += @{ + msg = "Removing a repo with this module is deprecated, use community.windows.win_psrepository to manage repos" + date = "2021-07-01" + collection_name = "community.windows" + } + + $Repo = (Get-PSRepository).Name + + # Try to remove the repository + if ($Repo -contains $Name) { + try { + if ( -not ($CheckMode) ) { + Unregister-PSRepository -Name $Name -ErrorAction Stop + } + $result.changed = $true + $result.repository_changed = $true + } + catch [ System.Exception ] { + $ErrorMessage = "Problems unregistering $($Name)repository: $($_.Exception.Message)" + Fail-Json $result $ErrorMessage + } + } +} + +# Check PowerShell version, fail if < 5.0 and required modules are not installed +$PsVersion = $PSVersionTable.PSVersion +if ($PsVersion.Major -lt 5 ) { + $PrereqModulesInstalled = Install-PrereqModule -TestInstallationOnly + if ( -not $PrereqModulesInstalled ) { + $ErrorMessage = -join @( + "Modules PowerShellGet and PackageManagement in versions 1.6.0 and 1.1.7 respectively " + "have to be installed before using the win_psmodule." + ) + Fail-Json $result $ErrorMessage + } +} + +if ( $required_version -and ( $minimum_version -or $maximum_version ) ) { + $ErrorMessage = "Parameters required_version and minimum/maximum_version are mutually exclusive." + Fail-Json $result $ErrorMessage +} + +if ( $allow_prerelease -and ( $minimum_version -or $maximum_version ) ) { + $ErrorMessage = "Parameters minimum_version, maximum_version can't be used with the parameter allow_prerelease." + Fail-Json $result $ErrorMessage +} + +if ( $allow_prerelease -and $state -eq "absent" ) { + $ErrorMessage = "The parameter allow_prerelease can't be used with state set to 'absent'." + Fail-Json $result $ErrorMessage +} + +if ( ($state -eq "latest") -and + ( $required_version -or $minimum_version -or $maximum_version ) ) { + $ErrorMessage = "When the parameter state is equal 'latest' you can use any of required_version, minimum_version, maximum_version." + Fail-Json $result $ErrorMessage +} + +if ( $repo -and (-not $url) ) { + $RepositoryExists = Get-PSRepository -Name $repo -ErrorAction SilentlyContinue + if ( $null -eq $RepositoryExists) { + $ErrorMessage = "The repository $repo doesn't exist." + Fail-Json $result $ErrorMessage + } + +} + +if ($repo_user -and $repo_pass ) { + $repo_credential = New-Object -TypeName PSCredential ($repo_user, ($repo_pass | ConvertTo-SecureString -AsPlainText -Force)) +} + +if ( ($allow_clobber -or $allow_prerelease -or $skip_publisher_check -or + $required_version -or $minimum_version -or $maximum_version -or $accept_license) ) { + # Update the PowerShellGet and PackageManagement modules. + # It's required to support AllowClobber, AllowPrerelease parameters. + # This must occur before PackageManagement or PowerShellGet is imported in + # the current process. + Install-PrereqModule -AllowClobber $allow_clobber -CheckMode $check_mode -AcceptLicense $accept_license -Repository $repo +} + +Import-Module -Name PackageManagement, PowerShellGet -Force + +if ($state -eq "present") { + if (($repo) -and ($url)) { + Install-Repository -Name $repo -Url $url -CheckMode $check_mode + } + else { + $ErrorMessage = "Repository Name and Url are mandatory if you want to add a new repository" + } + + if ($name) { + $ht = @{ + Name = $name + RequiredVersion = $required_version + MinimumVersion = $minimum_version + MaximumVersion = $maximum_version + Repository = $repo + AllowClobber = $allow_clobber + SkipPublisherCheck = $skip_publisher_check + AllowPrerelease = $allow_prerelease + CheckMode = $check_mode + Credential = $repo_credential + AcceptLicense = $accept_license + Force = $force + } + Install-PsModule @ht + } +} +elseif ($state -eq "absent") { + if ($repo) { + Remove-Repository -Name $repo -CheckMode $check_mode + } + + if ($name) { + $ht = @{ + Name = $Name + CheckMode = $check_mode + RequiredVersion = $required_version + MinimumVersion = $minimum_version + MaximumVersion = $maximum_version + } + Remove-PsModule @ht + } +} +elseif ( $state -eq "latest") { + + $ht = @{ + Name = $Name + AllowPrerelease = $allow_prerelease + Repository = $repo + CheckMode = $check_mode + Credential = $repo_credential + } + + $LatestVersion = Find-LatestPsModule @ht + + $ExistingModule = Get-PsModule $Name + + if ( ($LatestVersion.Version -ne $ExistingModule.Version) -or $force ) { + + $ht = @{ + Name = $Name + RequiredVersion = $LatestVersion + Repository = $repo + AllowClobber = $allow_clobber + SkipPublisherCheck = $skip_publisher_check + AllowPrerelease = $allow_prerelease + CheckMode = $check_mode + Credential = $repo_credential + AcceptLicense = $accept_license + Force = $force + } + Install-PsModule @ht + } +} + +Exit-Json $result diff --git a/ansible_collections/community/windows/plugins/modules/win_psmodule.py b/ansible_collections/community/windows/plugins/modules/win_psmodule.py new file mode 100644 index 000000000..ce2053aad --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_psmodule.py @@ -0,0 +1,186 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Wojciech Sciesinski <wojciech[at]sciesinski[dot]net> +# Copyright: (c) 2017, Daniele Lazzari <lazzari@mailup.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_psmodule +short_description: Adds or removes a Windows PowerShell module +description: + - This module helps to install Windows PowerShell modules and register custom modules repository on Windows-based systems. +options: + name: + description: + - Name of the Windows PowerShell module that has to be installed. + type: str + required: yes + state: + description: + - If C(present) a new module is installed. + - If C(absent) a module is removed. + - If C(latest) a module is updated to the newest version. + type: str + choices: [ absent, latest, present ] + default: present + required_version: + description: + - The exact version of the PowerShell module that has to be installed. + type: str + minimum_version: + description: + - The minimum version of the PowerShell module that has to be installed. + type: str + maximum_version: + description: + - The maximum version of the PowerShell module that has to be installed. + type: str + allow_clobber: + description: + - If C(yes) allows install modules that contains commands those have the same names as commands that already exists. + type: bool + default: no + skip_publisher_check: + description: + - If C(yes), allows you to install a different version of a module that already exists on your computer in the case when a different one + is not digitally signed by a trusted publisher and the newest existing module is digitally signed by a trusted publisher. + type: bool + default: no + allow_prerelease: + description: + - If C(yes) installs modules marked as prereleases. + - It doesn't work with the parameters C(minimum_version) and/or C(maximum_version). + - It doesn't work with the C(state) set to absent. + type: bool + default: no + repository: + description: + - Name of the custom repository to use. + type: str + username: + description: + - Username to authenticate against private repository. + type: str + required: no + version_added: '1.10.0' + password: + description: + - Password to authenticate against private repository. + type: str + required: no + version_added: '1.10.0' + accept_license: + description: + - Accepts the module's license. + - Required for modules that require license acceptance, since interactively answering the prompt is not possible. + - Corresponds to the C(-AcceptLicense) parameter of C(Install-Module). + - >- + Installation of a module or a dependency that requires license acceptance cannot be detected in check mode, + but will cause a failure at runtime unless I(accept_license=true). + type: bool + required: no + default: false + version_added: '1.11.0' + force: + description: + - Overrides warning messages about module installation conflicts + - If there is an existing module with the same name and version, force overwrites that version + - Corresponds to the C(-Force) parameter of C(Install-Module). + - >- + If module as dependency, it will also force reinstallation of dependency. + Updating them to latest if version isn't specified in module manifest. + type: bool + required: no + default: false + version_added: '1.13.0' + url: + description: + - URL of the custom repository to register. + - DEPRECATED, will be removed in a major release after C(2021-07-01), please use the + M(community.windows.win_psrepository) module instead. + type: str +notes: + - PowerShell modules needed + - PowerShellGet >= 1.6.0 + - PackageManagement >= 1.1.7 + - PowerShell package provider needed + - NuGet >= 2.8.5.201 + - On PowerShell 5.x required modules and a package provider will be updated under the first run of the win_psmodule module. + - On PowerShell 3.x and 4.x you have to install them before using the win_psmodule. +seealso: +- module: community.windows.win_psrepository +author: +- Wojciech Sciesinski (@it-praktyk) +- Daniele Lazzari (@dlazz) +''' + +EXAMPLES = r''' +--- +- name: Add a PowerShell module + community.windows.win_psmodule: + name: PowerShellModule + state: present + +- name: Add an exact version of PowerShell module + community.windows.win_psmodule: + name: PowerShellModule + required_version: "4.0.2" + state: present + +- name: Install or update an existing PowerShell module to the newest version + community.windows.win_psmodule: + name: PowerShellModule + state: latest + +- name: Install newer version of built-in Windows module + community.windows.win_psmodule: + name: Pester + skip_publisher_check: yes + state: present + +- name: Add a PowerShell module and register a repository + community.windows.win_psmodule: + name: MyCustomModule + repository: MyRepository + state: present + +- name: Add a PowerShell module from a specific repository + community.windows.win_psmodule: + name: PowerShellModule + repository: MyRepository + state: present + +- name: Add a PowerShell module from a specific repository with credentials + win_psmodule: + name: PowerShellModule + repository: MyRepository + username: repo_username + password: repo_password + state: present + +- name: Remove a PowerShell module + community.windows.win_psmodule: + name: PowerShellModule + state: absent +''' + +RETURN = r''' +--- +output: + description: A message describing the task result. + returned: always + sample: "Module PowerShellCookbook installed" + type: str +nuget_changed: + description: True when Nuget package provider is installed. + returned: always + type: bool + sample: true +repository_changed: + description: True when a custom repository is installed or removed. + returned: always + type: bool + sample: true +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_psmodule_info.ps1 b/ansible_collections/community/windows/plugins/modules/win_psmodule_info.ps1 new file mode 100644 index 000000000..b0932a79c --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_psmodule_info.ps1 @@ -0,0 +1,305 @@ +#!powershell + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# 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.CamelConversion +#Requires -Module PowerShellGet + +$spec = @{ + options = @{ + name = @{ type = 'str' ; default = '*' } + repository = @{ type = 'str' } + } + supports_check_mode = $true +} +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +# We need to remove this type data so that arrays don't get serialized weirdly. +# In some cases, an array gets serialized as an object with a Count and Value property where the value is the actual array. +# See: https://stackoverflow.com/a/48858780/3905079 +Remove-TypeData System.Array + +function Convert-ObjectToSnakeCase { + <# + .SYNOPSIS + Converts an object with CamelCase properties to a dictionary with snake_case keys. + Works in the spirit of and depends on the existing CamelConversion module util. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [OutputType([System.Collections.Specialized.OrderedDictionary])] + [Object] + $InputObject , + + [Parameter()] + [Switch] + $NoRecurse , + + [Parameter()] + [Switch] + $OmitNull + ) + + Process { + $result = [Ordered]@{} + foreach ($property in $InputObject.PSObject.Properties) { + $value = $property.Value + if (-not $NoRecurse -and $value -is [System.Collections.IDictionary]) { + $value = Convert-DictToSnakeCase -dict $value + } + elseif (-not $NoRecurse -and ($value -is [Array] -or $value -is [System.Collections.ArrayList])) { + $value = Convert-ListToSnakeCase -list $value + } + elseif ($null -eq $value) { + if ($OmitNull) { + continue + } + } + elseif (-not $NoRecurse -and $value -isnot [System.ValueType] -and $value -isnot [string]) { + $value = Convert-ObjectToSnakeCase -InputObject $value + } + + $name = Convert-StringToSnakeCase -string $property.Name + $result[$name] = $value + } + $result + } +} + +function ConvertTo-SerializableModuleInfo { + <# + .SYNOPSIS + Transforms some members of a ModuleInfo object to be more serialization-friendly and prevent infinite recursion. + + .DESCRIPTION + Stringifies version properties so we don't get serialized [System.Version] objects which aren't very useful. + + ExportedCommands and some other Exported* members are problematic in a few ways. + For one, they contain a reference to the full ModuleInfo object they are found in. This is probably useful + for nested modules and such, but in most cases they are just a reference to the current module, and this will + recurse infinitely. + + Further, the rest of the properties of the exported commands aren't needed for this module. A low depth on JSON + conversion doesn't fully work because some other module fields need it deeper than 1, and since this is a + dictionary we get a JSOn object whose property names and values are both just the name of the command. + + So instead we just make an array of the names, as that's good enough for what we want to return. + + NestedModules and RequiredModules are full ModuleInfo objects so that becomes a recursive call. + Limiting depth or reference counting shouldn't be necessary because the entries aren't references; they + have to actually exist. + + ModuleList gets recursed as well even though it's a tiny subset of fields, to transform versions and to + convert to snake_case. + + PrivateData can contain any data but a module manifest is a static file that can't contain references or + problematic types like [Type]. Unfortunately some module types like CIM (and presumably binary?) seem to be + able to populate that with whatever they want. + + As a precaution then, for module type that is not Script or Manifest, we limit PrivateData to a shallow depth. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Object] + $InputObject , + + [Parameter()] + [string[]] + $ExcludeProperty = @( + <# + Definition is the script that makes up the script module. + Big to return from this module, but also it doesn't ever seem to be filled in + in the data from Get-Module -ListAvailable. + #> + 'Definition', + <# + OnRemove is a script that gets run before the module is removed. + Not necessary for return. + #> + 'OnRemove', + <# + For binary modules, ImplementingAssembly is a reference to the assembly. + No use returning it. + #> + 'ImplementingAssembly', + <# + The session state instance of a loaded module. It's a runtime property only returned + without -ListAvailable and has no value serialized. + #> + 'SessionState' + ) + ) + + Process { + $properties = foreach ($p in $InputObject.PSObject.Properties) { + $pName = $p.Name + $pValue = $p.Value + + switch -Regex ($pName) { + '^PrivateData$' { + if ( + $InputObject.ModuleType -notin @( + [System.Management.Automation.ModuleType]::Script, + [System.Management.Automation.ModuleType]::Manifest + ) + ) { + $safeVal = $pValue | ConvertTo-Json -Depth 1 | ConvertFrom-Json + @{ + Name = $pName + Expression = { $safeVal }.GetNewClosure() + } + } + else { + $pName + } + break + } + '^(?:NestedModules|ModuleList|RequiredModules)$' { + # Nested and Required modules are full moduleinfo objects + # ModuleList isn't but its simplified fields need much of the same treatment + if ($pValue) { + @{ + Name = $pName + Expression = { + @(, ($pValue | ConvertTo-SerializableModuleInfo | Convert-ObjectToSnakeCase -NoRecurse)) + }.GetNewClosure() + } + } + else { + $pName + } + break + } + 'Version$' { + @{ + Name = $pName + Expression = { $pValue.ToString() }.GetNewClosure() + } + break + } + '^Exported' { + @{ + Name = $pName + Expression = { @(, $pValue.Keys) }.GetNewClosure() + } + break + } + default { + if ($pValue -is [datetime]) { + @{ + Name = $pName + Expression = { $pValue.ToString('o') }.GetNewClosure() + } + } + elseif ($pValue -is [enum] -or $pValue -is [type]) { + @{ + Name = $pName + Expression = { $pValue.ToString() }.GetNewClosure() + } + } + else { + $pName + } + } + } + } + + $InputObject | Select-Object -Property $properties -ExcludeProperty $ExcludeProperty + } +} + +function Add-ModuleRepositoryInfo { + <# + .SYNOPSIS + Takes a ModuleInfo object and adds some info that came from PowerShellGet + + .DESCRIPTION + Checks if there's information from Get-InstalledModule about the current module, + and if so adds to it to ModuleInfo object. The fields are always added, with null + values if need be. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [System.Object] + $InputObject , + + [Parameter()] + [String] + $RepositoryName + ) + + Begin { + $wantedProperties = @( + 'PublishedDate', + 'InstalledDate', + 'UpdatedDate', + 'Dependencies', + 'Repository', + 'PackageManagementProvider', + 'InstalledLocation', + 'RepositorySourceLocation', + 'Tags', + 'CompatiblePSEditions', + 'LicenseUri', + 'ProjectUri', + 'IconUri', + 'ReleaseNotes', + 'ExportedDscResources', # ExportedDscResources is not returned here, this is a hack for Windows 2012/R2 to ensure the field is present + 'Prefix' # Prefix is not actually returned here, this is a hack for Windows 2012 just to ensure the field is present + ) + + # Get all the installed modules at once. This prevents us from having to make an expensive individual call for every + # local module, the vast majority of which didn't come from PowerShellGet. + # The results here won't contain every version, but that's ok because we'll still have a fast signal as to whether + # it makes sense to make a version-specific call for the individual module. + $installedModules = Get-InstalledModule -ErrorAction SilentlyContinue | Group-Object -Property Name -AsHashTable -AsString + } + + Process { + $moduleName = $InputObject.Name + + $installed = if ($installedModules.Contains($moduleName)) { + # we know at least one version of this module was installed from PowerShellGet + # if the version of this local modle matches what we got it in the initial installed module list + # use it + if ($installedModules[$moduleName].Version -eq $InputObject.Version) { + $installedModules[$moduleName] + } + else { + # make an individual call to see if this specific version of the local module was installed from PowerShellGet + $im = Get-InstalledModule -Name $InputObject.Name -RequiredVersion $InputObject.Version -AllowPrerelease -ErrorAction SilentlyContinue + if ($im) { + $im + } + } + } + + if ($RepositoryName -and $installed.Repository -ne $RepositoryName) { + return + } + + $members = @{} + $wantedProperties | ForEach-Object -Process { + if (-not $InputObject.$_) { + # if the fields are present in both places, let's prefer what's sent in + $members[$_] = $installed.$_ + } + } + + $InputObject | Add-Member -NotePropertyMembers $members -Force -PassThru + } +} + +$module.Result.modules = @( + Get-Module -ListAvailable -Name $module.Params.name | + Add-ModuleRepositoryInfo -RepositoryName $module.Params.repository | + ConvertTo-SerializableModuleInfo | + Convert-ObjectToSnakeCase -NoRecurse +) + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_psmodule_info.py b/ansible_collections/community/windows/plugins/modules/win_psmodule_info.py new file mode 100644 index 000000000..4fe7c0c69 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_psmodule_info.py @@ -0,0 +1,435 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_psmodule_info +short_description: Gather information about PowerShell Modules +description: + - Gather information about PowerShell Modules including information from PowerShellGet. +options: + name: + description: + - The name of the module to retrieve. + - Supports any wildcard pattern supported by C(Get-Module). + - If omitted then all modules will returned. + type: str + default: '*' + repository: + description: + - The name of the PSRepository the modules were installed from. + - This acts as a filter against the modules that would be returned based on the I(name) option. + - Modules that were not installed from a repository will not be returned if this option is set. + - Only modules installed from a registered repository will be returned. + - If the repository was re-registered after module installation with a new C(SourceLocation), this will not match. + type: str +requirements: + - C(PowerShellGet) module +seealso: + - module: community.windows.win_psrepository_info + - module: community.windows.win_psscript_info +author: + - Brian Scholer (@briantist) +''' + +EXAMPLES = r''' +- name: Get info about all modules on the system + community.windows.win_psmodule_info: + +- name: Get info about the ScheduledTasks module + community.windows.win_psmodule_info: + name: ScheduledTasks + +- name: Get info about networking modules + community.windows.win_psmodule_info: + name: Net* + +- name: Get info about all modules installed from the PSGallery repository + community.windows.win_psmodule_info: + repository: PSGallery + register: gallery_modules + +- name: Update all modules retrieved from above example + community.windows.win_psmodule: + name: "{{ item }}" + state: latest + loop: "{{ gallery_modules.modules | map(attribute=name) }}" + +- name: Get info about all modules on the system + community.windows.win_psmodule_info: + register: all_modules + +- name: Find modules installed from a repository that isn't registered now + set_fact: + missing_repository_modules: "{{ + all_modules + | json_query('modules[?repository!=null && repository==repository_source_location].{name: name, version: version, repository: repository}') + | list + }}" + +- debug: + var: missing_repository_modules +''' + +RETURN = r''' +modules: + description: + - A list of modules (or an empty list is there are none). + returned: always + type: list + elements: dict + contains: + name: + description: + - The name of the module. + type: str + sample: PSReadLine + version: + description: + - The module version. + type: str + sample: 1.2.3 + guid: + description: + - The GUID of the module. + type: str + sample: 74c9fd30-734b-4c89-a8ae-7727ad21d1d5 + path: + description: + - The path to the module. + type: str + sample: 'C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules\PKI\PKI.psd1' + module_base: + description: + - The path that contains the module's files. + type: str + sample: 'C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules\PKI' + installed_location: + description: + - The path where the module is installed. + - This should have the same value as C(module_base) but only has a value when the module was installed via PowerShellGet. + type: str + sample: 'C:\Program Files\WindowsPowerShell\Modules\posh-git\0.7.1' + exported_aliases: + description: + - The aliases exported from the module. + type: list + elements: str + sample: + - glu + - slu + exported_cmdlets: + description: + - The cmdlets exported from the module. + type: list + elements: str + sample: + - Get-Certificate + - Get-PfxData + exported_commands: + description: + - All of the commands exported from the module. Includes functions, cmdlets, and aliases. + type: list + elements: str + sample: + - glu + - Get-LocalUser + exported_dsc_resources: + description: + - The DSC resources exported from the module. + type: list + elements: str + sample: + - xWebAppPool + - xWebSite + exported_format_files: + description: + - The format files exported from the module. + type: list + elements: path + sample: + - 'C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules\DnsClient\DnsCmdlets.Format.ps1xml' + - 'C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules\DnsClient\DnsConfig.Format.ps1xml' + - 'C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules\DnsClient\DnsClientPSProvider.Format.ps1xml' + exported_functions: + description: + - The functions exported from the module. + type: list + elements: str + sample: + - New-VirtualDisk + - New-Volume + exported_type_files: + description: + - The type files exported from the module. + type: list + elements: path + sample: + - 'C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules\DnsClient\DnsCmdlets.Types.ps1xml' + - 'C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules\DnsClient\DnsConfig.Types.ps1xml' + - 'C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules\DnsClient\DnsClientPSProvider.Types.ps1xml' + exported_variables: + description: + - The variables exported from the module. + type: list + elements: str + sample: + - GitPromptScriptBlock + exported_workflows: + description: + - The workflows exported from the module. + type: list + elements: str + access_mode: + description: + - The module's access mode. See U(https://docs.microsoft.com/en-us/dotnet/api/system.management.automation.moduleaccessmode) + type: str + sample: ReadWrite + module_type: + description: + - The module's type. See U(https://docs.microsoft.com/en-us/dotnet/api/system.management.automation.moduletype) + type: str + sample: Script + procoessor_architecture: + description: + - The module's processor architecture. See U(https://docs.microsoft.com/en-us/dotnet/api/system.reflection.processorarchitecture) + type: str + sample: Amd64 + author: + description: + - The author of the module. + type: str + sample: Warren Frame + copyright: + description: + - The copyright of the module. + type: str + sample: '(c) 2016 Warren F. All rights reserved.' + company_name: + description: + - The company name of the module. + type: str + sample: Microsoft Corporation + description: + description: + - The description of the module. + type: str + sample: Provides cmdlets to work with local users and local groups + clr_version: + description: + - The CLR version of the module. + type: str + sample: '4.0' + compatible_ps_editions: + description: + - The PS Editions the module is compatible with. + type: list + elements: str + sample: + - Desktop + dependencies: + description: + - The modules required by this module. + type: list + elements: str + dot_net_framework_version: + description: + - The .Net Framework version of the module. + type: str + sample: '4.6.1' + file_list: + description: + - The files included in the module. + type: list + elements: path + sample: + - 'C:\Program Files\WindowsPowerShell\Modules\PowerShellGet\1.6.0\PSModule.psm1' + - 'C:\Program Files\WindowsPowerShell\Modules\PowerShellGet\1.6.0\PSGet.Format.ps1xml' + - 'C:\Program Files\WindowsPowerShell\Modules\PowerShellGet\1.6.0\PSGet.Resource.psd1' + help_info_uri: + description: + - The help info address of the module. + type: str + sample: 'https://go.microsoft.com/fwlink/?linkid=390823' + icon_uri: + description: + - The address of the icon of the module. + type: str + sample: 'https://raw.githubusercontent.com/powershell/psscriptanalyzer/master/logo.png' + license_uri: + description: + - The address of the license for the module. + type: str + sample: 'https://github.com/PowerShell/xPendingReboot/blob/master/LICENSE' + project_uri: + description: + - The address of the module's project. + type: str + sample: 'https://github.com/psake/psake' + repository_source_location: + description: + - The source location of the repository where the module was installed from. + type: str + sample: 'https://www.powershellgallery.com/api/v2' + repository: + description: + - The PSRepository where the module was installed from. + - This value is not historical. It depends on the PSRepositories that are registered now for the current user. + - The C(repository_source_location) must match the current source location of a registered repository to get a repository name. + - If there is no match, then this value will match C(repository_source_location). + type: str + sample: PSGallery + release_notes: + description: + - The module's release notes. This is a free text field and no specific format should be assumed. + type: str + sample: | + ## 1.4.6 + - Update `HelpInfoUri` to point to the latest content + + ## 1.4.5 + - Bug fix for deadlock when getting parameters in an event + + ## 1.4.4 + - Bug fix when installing modules from private feeds + installed_date: + description: + - The date the module was installed. + type: str + sample: '2018-02-14T17:55:34.9620740-05:00' + published_date: + description: + - The date the module was published. + type: str + sample: '2017-03-15T04:18:09.0000000' + updated_date: + description: + - The date the module was last updated. + type: str + sample: '2019-12-31T09:20:02.0000000' + log_pipeline_execution_details: + description: + - Determines whether pipeline execution detail events should be logged. + type: bool + module_list: + description: + - A list of modules packaged with this module. + - This value is not often returned and the modules are not automatically processed. + type: list + elements: dict + contains: + name: + description: + - The name of the module. + - This may also be a path to the module file. + type: str + sample: '.\WindowsUpdateLog.psm1' + guid: + description: + - The GUID of the module. + type: str + sample: 82fdb72c-ecc5-4dfd-b9d5-83cf6eb9067f + version: + description: + - The minimum version of the module. + type: str + sample: '2.0' + maximum_version: + description: + - The maximum version of the module. + type: str + sample: '2.9' + required_version: + description: + - The exact version of the module required. + type: str + sample: '3.1.4' + nested_modules: + description: + - A list of modules nested with and loaded into the scope of this module. + - This list contains full module objects, so each item can have all of the properties listed here, including C(nested_modules). + type: list + elements: dict + required_modules: + description: + - A list of modules required by this module. + - This list contains full module objects, so each item can have all of the properties listed here, including C(required_modules). + - These module objects may not contain full information however, so you may see different results than if you had directly queried the module. + type: list + elements: dict + required_assemblies: + description: + - A list of assemblies that the module requires. + - The values may be a simple name or a full path. + type: str + sample: + - Microsoft.Management.Infrastructure.CimCmdlets.dll + - Microsoft.Management.Infrastructure.Dll + package_management_provider: + description: + - If the module was installed from PowerShellGet, this is the package management provider used. + type: str + sample: NuGet + power_shell_host_name: + description: + - The name of the PowerShell host that the module requires. + type: str + sample: Windows PowerShell ISE Host + power_shell_host_version: + description: + - The version of the PowerShell host that the module requires. + type: str + sample: '1.1' + power_shell_version: + description: + - The minimum version of PowerShell that the module requires. + type: str + sample: '5.1' + prefix: + description: + - The default prefix applied to C(Verb-Noun) commands exported from the module, resulting in C(Verb-PrefixNoun) names. + type: str + private_data: + description: + - Arbitrary private data used by the module. This is typically defined in the module manifest. + - This module limits the depth of the data returned for module types other than C(Script) and C(Manifest). + - The C(PSData) is commonly supplied and provides metadata for PowerShellGet but those fields are surfaced in top-level properties as well. + type: dict + sample: + PSData: + LicenseUri: https://example.com/module/LICENSE + ProjectUri: https://example.com/module/ + ReleaseNotes: | + v2 - Fixed some bugs + v1 - First release + Tags: + - networking + - serialization + root_module: + description: + - The root module as defined in the manifest. + - This may be a module name, filename, or full path. + type: str + sample: WindowsErrorReporting.psm1 + scripts: + description: + - A list of scripts (C(.ps1) files) that run in the caller's session state when the module is imported. + - This value comes from the C(ScriptsToProcess) field in the module's manifest. + type: list + sample: + - PrepareEnvironment.ps1 + - InitializeData.ps1 + tags: + description: + - The tags defined in the module's C(PSData) metadata. + type: list + elements: str + sample: + - networking + - serialization + - git + - dsc +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_psrepository.ps1 b/ansible_collections/community/windows/plugins/modules/win_psrepository.ps1 new file mode 100644 index 000000000..6e3319fd4 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_psrepository.ps1 @@ -0,0 +1,179 @@ +#!powershell + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# Copyright: (c) 2018, Wojciech Sciesinski <wojciech[at]sciesinski[dot]net> +# Copyright: (c) 2017, Daniele Lazzari <lazzari@mailup.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#Requires -Module Ansible.ModuleUtils.Legacy + +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'Some vars are referenced via Get-Variable')] +param() # param() is needed for attribute to take effect. + +# win_psrepository (Windows PowerShell repositories Additions/Removals/Updates) + +$params = Parse-Args -arguments $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 "str" -failifempty $true +$source_location = Get-AnsibleParam -obj $params -name "source_location" -aliases "source" -type "str" +$script_source_location = Get-AnsibleParam -obj $params -name "script_source_location" -type "str" +$publish_location = Get-AnsibleParam -obj $params -name "publish_location" -type "str" +$script_publish_location = Get-AnsibleParam -obj $params -name "script_publish_location" -type "str" +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present", "absent" +$installation_policy = Get-AnsibleParam -obj $params -name "installation_policy" -type "str" -validateset "trusted", "untrusted" +$force = Get-AnsibleParam -obj $params -name "force" -type "bool" -default $false +$proxy = Get-AnsibleParam -obj $params -name "proxy" -type "str" -failifempty $false +$repo_user = Get-AnsibleParam -obj $params -name "username" -type "str" +$repo_pass = Get-AnsibleParam -obj $params -name "password" -type "str" + +$result = @{"changed" = $false } + +# Enable TLS1.1/TLS1.2 if they're available but disabled (eg. .NET 4.5) +$security_protocols = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::SystemDefault +if ([System.Net.SecurityProtocolType].GetMember("Tls11").Count -gt 0) { + $security_protocols = $security_protocols -bor [System.Net.SecurityProtocolType]::Tls11 +} +if ([System.Net.SecurityProtocolType].GetMember("Tls12").Count -gt 0) { + $security_protocols = $security_protocols -bor [System.Net.SecurityProtocolType]::Tls12 +} +[System.Net.ServicePointManager]::SecurityProtocol = $security_protocols + +if (-not (Import-Module -Name PowerShellGet -MinimumVersion 1.6.0 -PassThru -ErrorAction SilentlyContinue)) { + Fail-Json -obj $result -Message "PowerShellGet version 1.6.0+ is required." +} + +$repository_params = @{ + Name = $name +} + +$Repo = Get-PSRepository @repository_params -ErrorAction SilentlyContinue + +if ($installation_policy) { + $repository_params.InstallationPolicy = $installation_policy +} + +if ($proxy) { + $repository_params.Proxy = $Proxy +} + +if ($repo_user -and $repo_pass ) { + $repository_params.Credential = New-Object -TypeName PSCredential ($repo_user, ($repo_pass | ConvertTo-SecureString -AsPlainText -Force)) +} + +function Resolve-LocationParameter { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string[]] + $Name , + + [Parameter(Mandatory = $true)] + [hashtable] + $Splatter + ) + + End { + foreach ($param in $Name) { + $val = Get-Variable -Name $param -ValueOnly -ErrorAction SilentlyContinue + if ($val) { + if ($val -as [uri]) { + $Splatter[$param.Replace('_', '')] = $val -as [uri] + } + else { + Fail-Json -obj $result -Message "'$param' must be a valid URI." + } + } + } + } +} + +Resolve-LocationParameter -Name source_location, publish_location, script_source_location, script_publish_location -Splatter $repository_params + +if (-not $repository_params.SourceLocation -and $state -eq 'present' -and ($force -or -not $Repo)) { + Fail-Json -obj $result -message "'source_location' is required when registering a new repository or using force with 'state' == 'present'." +} +Function Update-NuGetPackageProvider { + $PackageProvider = Get-PackageProvider -ListAvailable | Where-Object { ($_.name -eq 'Nuget') -and ($_.version -ge "2.8.5.201") } + if ($null -eq $PackageProvider) { + Find-PackageProvider -Name Nuget -ForceBootstrap -IncludeDependencies -Force | Out-Null + } +} + +if ($Repo) { + $changed_properties = @{} + + if ($force -and -not $repository_params.InstallationPolicy) { + $repository_params.InstallationPolicy = 'trusted' + } + + if ($repository_params.InstallationPolicy) { + if ($Repo.InstallationPolicy -ne $repository_params.InstallationPolicy) { + $changed_properties.InstallationPolicy = $repository_params.InstallationPolicy + } + } + + if ($repository_params.SourceLocation) { + # force check not needed here because it's done earlier; source_location is required with force + if ($repository_params.SourceLocation -ne $Repo.SourceLocation) { + $changed_properties.SourceLocation = $repository_params.SourceLocation + } + } + + if ($force -or $repository_params.ScriptSourceLocation) { + if ($repository_params.ScriptSourceLocation -ne $Repo.ScriptSourceLocation) { + $changed_properties.ScriptSourceLocation = $repository_params.ScriptSourceLocation + } + } + + if ($force -or $repository_params.PublishLocation) { + if ($repository_params.PublishLocation -ne $Repo.PublishLocation) { + $changed_properties.PublishLocation = $repository_params.PublishLocation + } + } + + if ($force -or $repository_params.ScriptPublishLocation) { + if ($repository_params.ScriptPublishLocation -ne $Repo.ScriptPublishLocation) { + $changed_properties.ScriptPublishLocation = $repository_params.ScriptPublishLocation + } + } + + if ($changed_properties.Count -gt 0) { + if ($repository_params.Credential) { + $changed_properties.Credential = $repository_params.Credential + } + } +} + +if ($Repo -and ($state -eq "absent" -or ($force -and $changed_properties.Count -gt 0))) { + if (-not $check_mode) { + Update-NuGetPackageProvider + Unregister-PSRepository -Name $name + } + $result.changed = $true +} + +if ($state -eq "present") { + if (-not $Repo -or ($force -and $changed_properties.Count -gt 0)) { + if (-not $repository_params.InstallationPolicy) { + $repository_params.InstallationPolicy = "trusted" + } + if (-not $check_mode) { + Update-NuGetPackageProvider + Register-PSRepository @repository_params + } + $result.changed = $true + } + else { + if ($changed_properties.Count -gt 0) { + if (-not $check_mode) { + Update-NuGetPackageProvider + Set-PSRepository -Name $name @changed_properties + } + $result.changed = $true + } + } +} + +Exit-Json -obj $result diff --git a/ansible_collections/community/windows/plugins/modules/win_psrepository.py b/ansible_collections/community/windows/plugins/modules/win_psrepository.py new file mode 100644 index 000000000..6c37b204b --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_psrepository.py @@ -0,0 +1,153 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# Copyright: (c) 2018, Wojciech Sciesinski <wojciech[at]sciesinski[dot]net> +# Copyright: (c) 2017, Daniele Lazzari <lazzari@mailup.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_psrepository +short_description: Adds, removes or updates a Windows PowerShell repository. +description: + - This module helps to add, remove and update Windows PowerShell repository on Windows-based systems. +options: + name: + description: + - Name of the repository to work with. + type: str + required: yes + source_location: + description: + - Specifies the URI for discovering and installing modules from this repository. + - A URI can be a NuGet server feed (most common situation), HTTP, HTTPS, FTP or file location. + - Required when registering a new repository or using I(force=True). + type: str + aliases: + - source + script_source_location: + description: + - Specifies the URI for discovering and installing scripts from this repository. + type: str + publish_location: + description: + - Specifies the URI for publishing modules to this repository. + type: str + script_publish_location: + description: + - Specifies the URI for publishing scripts to this repository. + type: str + state: + description: + - If C(present) a new repository is added or updated. + - If C(absent) a repository is removed. + type: str + choices: [ absent, present ] + default: present + installation_policy: + description: + - Sets the C(InstallationPolicy) of a repository. + - Will default to C(trusted) when creating a new repository or used with I(force=True). + type: str + choices: [ trusted, untrusted ] + force: + description: + - If C(True), any differences from the desired state will result in the repository being unregistered, and then re-registered. + - I(force) has no effect when I(state=absent). See notes for additional context. + type: bool + default: False + proxy: + description: + - Proxy to use for repository. + type: str + required: no + version_added: 1.1.0 + username: + description: + - Username to authenticate against private repository. + type: str + required: no + version_added: '1.10.0' + password: + description: + - Password to authenticate against private repository. + type: str + required: no + version_added: '1.10.0' +requirements: + - PowerShell Module L(PowerShellGet >= 1.6.0,https://www.powershellgallery.com/packages/PowerShellGet/) + - PowerShell Module L(PackageManagement >= 1.1.7,https://www.powershellgallery.com/packages/PackageManagement/) + - PowerShell Package Provider C(NuGet) >= 2.8.5.201 +notes: + - See the examples on how to update the NuGet package provider. + - You can not use C(win_psrepository) to re-register (add) removed PSGallery, use the command C(Register-PSRepository -Default) instead. + - When registering or setting I(source_location), PowerShellGet will transform the location according to internal rules, such as following HTTP/S redirects. + - This can result in a C(CHANGED) status on each run as the values will never match and will be "reset" each time. + - To work around that, find the true destination value with M(community.windows.win_psrepository_info) or C(Get-PSRepository) and update the playbook to + match. + - When updating an existing repository, all options except I(name) are optional. Only supplied options will be updated. Use I(force=True) to exactly match. + - I(script_location), I(publish_location), and I(script_publish_location) are optional but once set can only be cleared with I(force=True). + - Using I(force=True) will unregister and re-register the repository if there are any changes, so that it exactly matches the options specified. +seealso: + - module: community.windows.win_psrepository_info + - module: community.windows.win_psmodule +author: + - Wojciech Sciesinski (@it-praktyk) + - Brian Scholer (@briantist) +''' + +EXAMPLES = r''' +--- +- name: Ensure the required NuGet package provider version is installed + ansible.windows.win_shell: Find-PackageProvider -Name Nuget -ForceBootstrap -IncludeDependencies -Force + +- name: Register a PowerShell repository + community.windows.win_psrepository: + name: MyRepository + source_location: https://myrepo.com + state: present + +- name: Remove a PowerShell repository + community.windows.win_psrepository: + name: MyRepository + state: absent + +- name: Add an untrusted repository + community.windows.win_psrepository: + name: MyRepository + installation_policy: untrusted + +- name: Add a repository with different locations + community.windows.win_psrepository: + name: NewRepo + source_location: https://myrepo.example/module/feed + script_source_location: https://myrepo.example/script/feed + publish_location: https://myrepo.example/api/module/publish + script_publish_location: https://myrepo.example/api/script/publish + +- name: Update only two properties on the above repository + community.windows.win_psrepository: + name: NewRepo + installation_policy: untrusted + script_publish_location: https://scriptprocessor.example/publish + +- name: Clear script locations from the above repository by re-registering it + community.windows.win_psrepository: + name: NewRepo + installation_policy: untrusted + source_location: https://myrepo.example/module/feed + publish_location: https://myrepo.example/api/module/publish + force: True + +- name: Register a PowerShell repository with credentials + community.windows.win_psrepository: + name: MyRepository + source_location: https://myrepo.com + state: present + username: repo_username + password: repo_password +''' + +RETURN = ''' +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_psrepository_copy.ps1 b/ansible_collections/community/windows/plugins/modules/win_psrepository_copy.ps1 new file mode 100644 index 000000000..47c6cece6 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_psrepository_copy.ps1 @@ -0,0 +1,233 @@ +#!powershell + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic + +$spec = @{ + supports_check_mode = $true + options = @{ + source = @{ + type = 'path' + default = '%LOCALAPPDATA%\Microsoft\Windows\PowerShell\PowerShellGet\PSRepositories.xml' + } + name = @{ + type = 'list' + elements = 'str' + default = @( + '*' + ) + } + exclude = @{ + type = 'list' + elements = 'str' + } + profiles = @{ + type = 'list' + elements = 'str' + default = @( + '*' + ) + } + exclude_profiles = @{ + type = 'list' + elements = 'str' + default = @( + 'systemprofile' + 'LocalService' + 'NetworkService' + ) + } + } +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$module.Diff.before = @{} +$module.Diff.after = @{} + +function Select-Wildcard { + <# + .SYNOPSIS + Compares a value to an Include and Exclude list of wildcards, + returning the input object if a match is found + + .DESCRIPTION + If $Property is specified, that property of the input object is + compared rather than the object itself, but the original object + is returned, not the property. + #> + [CmdletBinding()] + [OutputType([object])] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [object] + $InputObject , + + [Parameter()] + [String] + $Property , + + [Parameter()] + [String[]] + $Include , + + [Parameter()] + [String[]] + $Exclude + ) + + Process { + $o = if ($Property) { + $InputObject.($Property) + } + else { + $InputObject + } + + foreach ($inc in $Include) { + $imatch = $o -like $inc + if ($imatch) { + break + } + } + + if (-not $imatch) { + return + } + + foreach ($exc in $Exclude) { + if ($o -like $exc) { + return + } + } + + $InputObject + } +} + +function Get-ProfileDirectory { + <# + .SYNOPSIS + Returns DirectoryInfo objects for each profile on the system, as reported by the registry + + .DESCRIPTION + The special "Default" profile, used as a template for newly created users, is explicitly + added to the list of possible profiles returned. Public is explicitly excluded. + Paths reported by the registry that don't exist on the filesystem are silently skipped. + #> + [CmdletBinding()] + [OutputType([System.IO.DirectoryInfo])] + param( + [Parameter()] + [String[]] + $Include , + + [Parameter()] + [String[]] + $Exclude + ) + + $regPL = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' + + # note: this is a key named "Default", not the (Default) key + $default = Get-ItemProperty -LiteralPath $regPL | Select-Object -ExpandProperty Default + + # "ProfileImagePath" is always the local side of the profile, even if roaming profiles are used + # This is what we want, because PSRepositories are stored in AppData/Local and don't roam + $profiles = ( + @($default) + + (Get-ChildItem -LiteralPath $regPL | Get-ItemProperty | Select-Object -ExpandProperty ProfileImagePath) + ) -as [System.IO.DirectoryInfo[]] + + $profiles | + Where-Object -Property Exists -EQ $true | + Select-Wildcard -Property Name -Include $Include -Exclude $Exclude +} + +function Compare-Hashtable { + <# + .SYNOPSIS + Attempts to naively compare two hashtables by serializing them and string comparing the serialized versions + #> + [CmdletBinding()] + [OutputType([bool])] + param( + [Parameter(Mandatory)] + [hashtable] + $ReferenceObject , + + [Parameter(Mandatory)] + [hashtable] + $DifferenceObject , + + [Parameter()] + [int] + $Depth + ) + + if ($PSBoundParameters.ContainsKey('Depth')) { + $sRef = [System.Management.Automation.PSSerializer]::Serialize($ReferenceObject, $Depth) + $sDif = [System.Management.Automation.PSSerializer]::Serialize($DifferenceObject, $Depth) + } + else { + $sRef = [System.Management.Automation.PSSerializer]::Serialize($ReferenceObject) + $sDif = [System.Management.Automation.PSSerializer]::Serialize($DifferenceObject) + } + + $sRef -ceq $sDif +} + +# load the repositories from the source file +try { + $src = $module.Params.source + $src_repos = Import-Clixml -LiteralPath $src -ErrorAction Stop +} +catch [System.IO.FileNotFoundException] { + $module.FailJson("The source file '$src' was not found.", $_) +} +catch { + $module.FailJson("There was an error loading the source file '$src': $($_.Exception.Message).", $_) +} + +$profiles = Get-ProfileDirectory -Include $module.Params.profiles -Exclude $module.Params.exclude_profiles + +foreach ($user in $profiles) { + $username = $user.Name + + $repo_dir = $user.FullName | Join-Path -ChildPath 'AppData\Local\Microsoft\Windows\PowerShell\PowerShellGet' + $repo_path = $repo_dir | Join-Path -ChildPath 'PSRepositories.xml' + + if (Test-Path -LiteralPath $repo_path) { + $cur_repos = Import-Clixml -LiteralPath $repo_path + } + else { + $cur_repos = @{} + } + + $new_repos = $cur_repos.Clone() + $updated = $false + + $src_repos.Keys | + Select-Wildcard -Include $module.Params.name -Exclude $module.Params.exclude | + ForEach-Object -Process { + # explicit scope used inside ForEach-Object to satisfy lint (PSUseDeclaredVarsMoreThanAssignment) + # see https://github.com/PowerShell/PSScriptAnalyzer/issues/827 + $Script:updated = $true + $Script:new_repos[$_] = $Script:src_repos[$_] + } + + $module.Diff.before[$username] = $cur_repos + $module.Diff.after[$username] = $new_repos + + if ($updated -and -not (Compare-Hashtable -ReferenceObject $cur_repos -DifferenceObject $new_repos)) { + if (-not $module.CheckMode) { + $null = New-Item -Path $repo_dir -ItemType Directory -Force -ErrorAction SilentlyContinue + $new_repos | Export-Clixml -LiteralPath $repo_path -Force + } + $module.Result.changed = $true + } +} + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_psrepository_copy.py b/ansible_collections/community/windows/plugins/modules/win_psrepository_copy.py new file mode 100644 index 000000000..686c00625 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_psrepository_copy.py @@ -0,0 +1,143 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_psrepository_copy +short_description: Copies registered PSRepositories to other user profiles +version_added: '1.3.0' +description: + - Copies specified registered PSRepositories to other user profiles on the system. + - Can include the C(Default) profile so that new users start with the selected repositories. + - Can include special service accounts like the local SYSTEM user, LocalService, NetworkService. +options: + source: + description: + - The full path to the source repositories XML file. + - Defaults to the repositories registered to the current user. + type: path + default: '%LOCALAPPDATA%\Microsoft\Windows\PowerShell\PowerShellGet\PSRepositories.xml' + name: + description: + - The names of repositories to copy. + - Names are interpreted as wildcards. + type: list + elements: str + default: ['*'] + exclude: + description: + - The names of repositories to exclude. + - Names are interpreted as wildcards. + - If a name matches both an include (I(name)) and I(exclude), it will be excluded. + type: list + elements: str + profiles: + description: + - The names of user profiles to populate with repositories. + - Names are interpreted as wildcards. + - The C(Default) profile can also be matched. + - The C(Public) and C(All Users) profiles cannot be targeted, as PSRepositories are not loaded from them. + type: list + elements: str + default: ['*'] + exclude_profiles: + description: + - The names of user profiles to exclude. + - If a profile matches both an include (I(profiles)) and I(exclude_profiles), it will be excluded. + - By default, the service account profiles are excluded. + - To explcitly exclude nothing, set I(exclude_profiles=[]). + type: list + elements: str + default: + - systemprofile + - LocalService + - NetworkService +notes: + - Does not require the C(PowerShellGet) module or any other external dependencies. + - User profiles are loaded from the registry. If a given path does not exist (like if the profile directory was deleted), it is silently skipped. + - If setting service account profiles, you may need C(become=yes). See examples. + - "When PowerShellGet first sets up a repositories file, it always adds C(PSGallery), however if this module creates a new repos file and your selected + repositories don't include C(PSGallery), it won't be in your destination." + - "The values searched in I(profiles) (and I(exclude_profiles)) are profile names, not necessarily user names. This can happen when the profile path is + deliberately changed or when domain user names conflict with users from the local computer or another domain. In this case the second+ user may have the + domain name or local computer name appended, like C(JoeUser.Contoso) vs. C(JoeUser). + If you intend to filter user profiles, ensure your filters catch the right names." + - "In the case of the service accounts, the specific profiles are C(systemprofile) (for the C(SYSTEM) user), and C(LocalService) or C(NetworkService) + for those accounts respectively." + - "Repositories with credentials (requiring authentication) or proxy information will copy, but the credentials and proxy details will not as that + information is not stored with repository." +seealso: + - module: community.windows.win_psrepository + - module: community.windows.win_psrepository_info +author: + - Brian Scholer (@briantist) +''' + +EXAMPLES = r''' +- name: Copy the current user's PSRepositories to all non-service account profiles and Default profile + community.windows.win_psrepository_copy: + +- name: Copy the current user's PSRepositories to all profiles and Default profile + community.windows.win_psrepository_copy: + exclude_profiles: [] + +- name: Copy the current user's PSRepositories to all profiles beginning with A, B, or C + community.windows.win_psrepository_copy: + profiles: + - 'A*' + - 'B*' + - 'C*' + +- name: Copy the current user's PSRepositories to all profiles beginning B except Brian and Brianna + community.windows.win_psrepository_copy: + profiles: 'B*' + exclude_profiles: + - Brian + - Brianna + +- name: Copy a specific set of repositories to profiles beginning with 'svc' with exceptions + community.windows.win_psrepository_copy: + name: + - CompanyRepo1 + - CompanyRepo2 + - PSGallery + profiles: 'svc*' + exclude_profiles: 'svc-restricted' + +- name: Copy repos matching a pattern with exceptions + community.windows.win_psrepository_copy: + name: 'CompanyRepo*' + exclude: 'CompanyRepo*-Beta' + +- name: Copy repositories from a custom XML file on the target host + community.windows.win_psrepository_copy: + source: 'C:\data\CustomRepostories.xml' + +### A sample workflow of seeding a system with a custom repository + +# A playbook that does initial host setup or builds system images + +- name: Register custom respository + community.windows.win_psrepository: + name: PrivateRepo + source_location: https://example.com/nuget/feed/etc + installation_policy: trusted + +- name: Ensure all current and new users have this repository registered + community.windows.win_psrepository_copy: + name: PrivateRepo + +# In another playbook, run by other users (who may have been created later) + +- name: Install a module + community.windows.win_psmodule: + name: CompanyModule + repository: PrivateRepo + state: present +''' + +RETURN = r''' +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_psrepository_info.ps1 b/ansible_collections/community/windows/plugins/modules/win_psrepository_info.ps1 new file mode 100644 index 000000000..c0a7545b5 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_psrepository_info.ps1 @@ -0,0 +1,68 @@ +#!powershell + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# 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.CamelConversion +#Requires -Module PowerShellGet + +$spec = @{ + options = @{ + name = @{ type = 'str' ; default = '*' } + } + supports_check_mode = $true +} +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +function Convert-ObjectToSnakeCase { + <# + .SYNOPSIS + Converts an object with CamelCase properties to a dictionary with snake_case keys. + Works in the spirit of and depends on the existing CamelConversion module util. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [OutputType([System.Collections.Specialized.OrderedDictionary])] + [Object] + $InputObject , + + [Parameter()] + [Switch] + $NoRecurse , + + [Parameter()] + [Switch] + $OmitNull + ) + + Process { + $result = [Ordered]@{} + foreach ($property in $InputObject.PSObject.Properties) { + $value = $property.Value + if (-not $NoRecurse -and $value -is [System.Collections.IDictionary]) { + $value = Convert-DictToSnakeCase -dict $value + } + elseif (-not $NoRecurse -and ($value -is [Array] -or $value -is [System.Collections.ArrayList])) { + $value = Convert-ListToSnakeCase -list $value + } + elseif ($null -eq $value) { + if ($OmitNull) { + continue + } + } + elseif (-not $NoRecurse -and $value -isnot [System.ValueType] -and $value -isnot [string]) { + $value = Convert-ObjectToSnakeCase -InputObject $value + } + + $name = Convert-StringToSnakeCase -string $property.Name + $result[$name] = $value + } + $result + } +} + +$module.Result.repositories = @(Get-PSRepository -Name $module.Params.name | Convert-ObjectToSnakeCase) + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_psrepository_info.py b/ansible_collections/community/windows/plugins/modules/win_psrepository_info.py new file mode 100644 index 000000000..9a54d041f --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_psrepository_info.py @@ -0,0 +1,107 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_psrepository_info +short_description: Gather information about PSRepositories +description: + - Gather information about all or a specific PSRepository. +options: + name: + description: + - The name of the repository to retrieve. + - Supports any wildcard pattern supported by C(Get-PSRepository). + - If omitted then all repositories will returned. + type: str + default: '*' +requirements: + - C(PowerShellGet) module +seealso: + - module: community.windows.win_psrepository +author: + - Brian Scholer (@briantist) +''' + +EXAMPLES = r''' +- name: Get info for a single repository + community.windows.win_psrepository_info: + name: PSGallery + register: repo_info + +- name: Find all repositories that start with 'MyCompany' + community.windows.win_psrepository_info: + name: MyCompany* + +- name: Get info for all repositories + community.windows.win_psrepository_info: + register: repo_info + +- name: Remove all repositories that don't have a publish_location set + community.windows.win_psrepository: + name: "{{ item }}" + state: absent + loop: "{{ repo_info.repositories | rejectattr('publish_location', 'none') | list }}" +''' + +RETURN = r''' +repositories: + description: + - A list of repositories (or an empty list is there are none). + returned: always + type: list + elements: dict + contains: + name: + description: + - The name of the repository. + type: str + sample: PSGallery + installation_policy: + description: + - The installation policy of the repository. The sample values are the only possible values. + type: str + sample: + - Trusted + - Untrusted + trusted: + description: + - A boolean flag reflecting the value of C(installation_policy) as to whether the repository is trusted. + type: bool + package_management_provider: + description: + - The name of the package management provider for this repository. + type: str + sample: NuGet + provider_options: + description: + - Provider-specific options for this repository. + type: dict + source_location: + description: + - The location used to find and retrieve modules. This should always have a value. + type: str + sample: https://www.powershellgallery.com/api/v2 + publish_location: + description: + - The location used to publish modules. + type: str + sample: https://www.powershellgallery.com/api/v2/package/ + script_source_location: + description: + - The location used to find and retrieve scripts. + type: str + sample: https://www.powershellgallery.com/api/v2/items/psscript + script_publish_location: + description: + - The location used to publish scripts. + type: str + sample: https://www.powershellgallery.com/api/v2/package/ + registered: + description: + - Whether the module is registered. Should always be C(True) + type: bool +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_psscript.ps1 b/ansible_collections/community/windows/plugins/modules/win_psscript.ps1 new file mode 100644 index 000000000..dc54741f8 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_psscript.ps1 @@ -0,0 +1,198 @@ +#!powershell + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic +#Requires -Module @{ ModuleName = 'PowerShellGet' ; ModuleVersion = '1.6.0' } + +$spec = @{ + supports_check_mode = $true + options = @{ + name = @{ + type = 'str' + required = $true + } + repository = @{ type = 'str' } + scope = @{ + type = 'str' + choices = @('all_users', 'current_user') + default = 'all_users' + } + state = @{ + type = 'str' + choices = @('present', 'absent', 'latest') + default = 'present' + } + required_version = @{ type = 'str' } + minimum_version = @{ type = 'str' } + maximum_version = @{ type = 'str' } + source_username = @{ type = 'str' } + source_password = @{ + type = 'str' + no_log = $true + } + allow_prerelease = @{ + type = 'bool' + default = $false + } + } + + mutually_exclusive = @( + @('required_version', 'minimum_version'), + @('required_version', 'maximum_version') + ) + + required_together = @( + , @('source_username', 'source_password') + ) +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) +$state = $module.Params.state + +# Enable TLS1.1/TLS1.2 if they're available but disabled (eg. .NET 4.5) +$security_protocols = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::SystemDefault +if ([System.Net.SecurityProtocolType].GetMember("Tls11").Count -gt 0) { + $security_protocols = $security_protocols -bor [System.Net.SecurityProtocolType]::Tls11 +} +if ([System.Net.SecurityProtocolType].GetMember("Tls12").Count -gt 0) { + $security_protocols = $security_protocols -bor [System.Net.SecurityProtocolType]::Tls12 +} +[System.Net.ServicePointManager]::SecurityProtocol = $security_protocols + +function Get-SplattableParameter { + [CmdletBinding(DefaultParameterSetName = 'All')] + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [Ansible.Basic.AnsibleModule] + $Module , + + [Parameter(ParameterSetName = 'ForCommand')] + [Alias('For')] + [String] + $ForCommand , + + [Parameter(ParameterSetName = 'WithCommand', ValueFromPipeline = $true)] + [Alias('Command')] + [System.Management.Automation.CommandInfo] + $WithCommand + ) + + Process { + $ret = @{} + + $cmd = switch ($PSCmdlet.ParameterSetName) { + 'WithCommand' { $WithCommand } + 'ForCommand' { Get-Command -Name $ForCommand } + } + + if ($cmd) { + $commons = [System.Management.Automation.Cmdlet]::CommonParameters + [System.Management.Automation.Cmdlet]::CommonOptionalParameters + $validParams = $cmd.Parameters.GetEnumerator() | ForEach-Object -Process { + if ($commons -notcontains $_.Key) { + $_.Key + } + } + } + + switch -Wildcard ($Module.Params.Keys) { + '*' { + $value = $Module.Params[$_] + if ($null -eq $value) { + continue + } + + $key = $_.Replace('_', '') + if ($validParams -and $validParams -notcontains $key) { + continue + } + } + + 'scope' { $value = $value.Replace('_', '') } + 'source_username' { continue } # handled in password block + 'source_password' { + $key = 'Credential' + $secure = ConvertTo-SecureString -String $value -AsPlainText -Force + $value = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Module.Params.source_username, $secure + } + '*_version' { + if ($Module.Params.state -eq 'latest') { + $Module.FailJson("version options can't be used with 'state': 'latest'") + } + } + 'repository' { + if ($Module.Params.state -eq 'absent') { + $Module.FailJson("'repository' is not valid with 'state': 'absent'") + } + } + + '*' { $ret[$key] = $value } + } + + $ret + } +} + +$pGet, $pUninstall, $pFind, $pInstall = Get-Command -Name @( + 'Get-InstalledScript' + , 'Uninstall-Script' + , 'Find-Script' + , 'Install-Script' +) | Get-SplattableParameter -Module $module + +$existing = Get-InstalledScript @pGet -ErrorAction SilentlyContinue +$existing = if ($existing) { + $existing | Group-Object -Property Name -AsHashTable -AsString +} +else { + @{} +} + +if ($state -eq 'absent') { + if ($existing.Count) { + try { + $module.Result.changed = $true + if (-not $module.CheckMode) { + $existing.Values | Uninstall-Script -Force -ErrorAction Stop + } + } + catch { + $module.FailJson("Error uninstalling scripts.", $_) + } + } +} +else { + # state is 'present' or 'latest' + try { + $remote = Find-Script @pFind -ErrorAction Stop + } + catch { + $module.FailJson("Error searching for scripts.", $_) + } + + try { + $toInstall = $remote | Where-Object -FilterScript { + -not $existing.ContainsKey($_.Name) -or ( + $state -eq 'latest' -and + ($_.Version -as [version]) -gt ($existing[$_.Name].Version -as [version]) + ) + } + + if (($toInstall | Group-Object -Property Name -NoElement | Where-Object -Property Count -gt 1)) { + $module.FailJson("Multiple scripts found. Please choose a specific repository.") + } + + $module.Result.changed = $toInstall -as [bool] + + if ($toInstall -and -not $module.CheckMode) { + $toInstall | Install-Script -Scope:$pInstall.scope -Force -ErrorAction Stop + } + } + catch { + $module.FailJson("Error installing scripts.", $_) + } +} + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_psscript.py b/ansible_collections/community/windows/plugins/modules/win_psscript.py new file mode 100644 index 000000000..00269d037 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_psscript.py @@ -0,0 +1,128 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_psscript +short_description: Install and manage PowerShell scripts from a PSRepository +description: + - Add or remove PowerShell scripts from registered PSRepositories. +options: + name: + description: + - The name of the script you want to install or remove. + type: str + required: True + repository: + description: + - The registered name of the repository you want to install from. + - Cannot be used when I(state=absent). + - If ommitted, all repositories will be searched. + - To register a repository, use M(community.windows.win_psrepository). + type: str + scope: + description: + - Determines whether the script is installed for only the C(current_user) or for C(all_users). + type: str + choices: + - current_user + - all_users + default: all_users + state: + description: + - The desired state of the script. C(absent) removes the script. + - C(latest) will ensure the most recent version available is installed. + - C(present) only installs if the script is missing. + type: str + choices: + - present + - absent + - latest + default: present + required_version: + description: + - The exact version of the script to install. + - Cannot be used with I(minimum_version) or I(maximum_version). + - Cannot be used when I(state=latest). + type: str + minimum_version: + description: + - The minimum version of the script to install. + - Cannot be used when I(state=latest). + type: str + maximum_version: + description: + - The maximum version of the script to install. + - Cannot be used when I(state=latest). + type: str + allow_prerelease: + description: + - If C(yes) installs scripts flagged as prereleases. + type: bool + default: no + source_username: + description: + - The username portion of the credential required to access the repository. + - Must be used together with I(source_password). + type: str + source_password: + description: + - The password portion of the credential required to access the repository. + - Must be used together with I(source_username). + type: str +requirements: + - C(PowerShellGet) module v1.6.0+ +seealso: + - module: community.windows.win_psrepository + - module: community.windows.win_psrepository_info + - module: community.windows.win_psmodule +notes: + - Unlike PowerShell modules, scripts do not support side-by-side installations of multiple versions. Installing a new version will replace the existing one. +author: + - Brian Scholer (@briantist) +''' + +EXAMPLES = r''' +- name: Install a script from PSGallery + community.windows.win_psscript: + name: Test-RPC + repository: PSGallery + +- name: Find and install the latest version of a script from any repository + community.windows.win_psscript: + name: Get-WindowsAutoPilotInfo + state: latest + +- name: Remove a script that isn't needed + community.windows.win_psscript: + name: Defrag-Partition + state: absent + +- name: Install a specific version of a script for the current user + community.windows.win_psscript: + name: CleanOldFiles + scope: current_user + required_version: 3.10.2 + +- name: Install a script below a certain version + community.windows.win_psscript: + name: New-FeatureEnable + maximum_version: 2.99.99 + +- name: Ensure a minimum version of a script is present + community.windows.win_psscript: + name: OldStandby + minimum_version: 3.0.0 + +- name: Install any available version that fits a specific range + community.windows.win_psscript: + name: FinickyScript + minimum_version: 2.5.1 + maximum_version: 2.6.19 +''' + +RETURN = r''' +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_psscript_info.ps1 b/ansible_collections/community/windows/plugins/modules/win_psscript_info.ps1 new file mode 100644 index 000000000..8f6cc9082 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_psscript_info.ps1 @@ -0,0 +1,129 @@ +#!powershell + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# 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.CamelConversion +#Requires -Module PowerShellGet + +$spec = @{ + options = @{ + name = @{ type = 'str' ; default = '*' } + repository = @{ type = 'str' } + } + supports_check_mode = $true +} +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +function Convert-ObjectToSnakeCase { + <# + .SYNOPSIS + Converts an object with CamelCase properties to a dictionary with snake_case keys. + Works in the spirit of and depends on the existing CamelConversion module util. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [OutputType([System.Collections.Specialized.OrderedDictionary])] + [Object] + $InputObject , + + [Parameter()] + [Switch] + $NoRecurse , + + [Parameter()] + [Switch] + $OmitNull + ) + + Process { + $result = [Ordered]@{} + foreach ($property in $InputObject.PSObject.Properties) { + $value = $property.Value + if (-not $NoRecurse -and $value -is [System.Collections.IDictionary]) { + $value = Convert-DictToSnakeCase -dict $value + } + elseif (-not $NoRecurse -and ($value -is [Array] -or $value -is [System.Collections.ArrayList])) { + $value = Convert-ListToSnakeCase -list $value + } + elseif ($null -eq $value) { + if ($OmitNull) { + continue + } + } + elseif (-not $NoRecurse -and $value -isnot [System.ValueType] -and $value -isnot [string]) { + $value = Convert-ObjectToSnakeCase -InputObject $value + } + + $name = Convert-StringToSnakeCase -string $property.Name + $result[$name] = $value + } + $result + } +} + +function ConvertTo-SerializableScriptInfo { + <# + .SYNOPSIS + Transforms some members of a PSRepositoryItemInfo object to be more serialization-friendly. + + .DESCRIPTION + Stringifies [DateTime], [enum], and [type] values for serialization + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Object] + $InputObject , + + [Parameter()] + [string[]] + $ExcludeProperty = @( + <# + Includes is for modules, containing the stuff they export. + #> + 'Includes' , + + <# + This is always 'Script' for scripts. + #> + 'Type' + ) + ) + + Process { + $properties = foreach ($p in $InputObject.PSObject.Properties) { + $pName = $p.Name + $pValue = $p.Value + + if ($pValue -is [datetime]) { + @{ + Name = $pName + Expression = { $pValue.ToString('o') }.GetNewClosure() + } + } + elseif ($pValue -is [enum] -or $pValue -is [type]) { + @{ + Name = $pName + Expression = { $pValue.ToString() }.GetNewClosure() + } + } + else { + $pName + } + } + + $InputObject | Select-Object -Property $properties -ExcludeProperty $ExcludeProperty + } +} + +$module.Result.scripts = @( + Get-InstalledScript -Name $module.Params.name -ErrorAction SilentlyContinue | + Where-Object -FilterScript { -not $module.Params.repository -or $_.Repository -eq $module.Params.repository } | + ConvertTo-SerializableScriptInfo | + Convert-ObjectToSnakeCase -NoRecurse +) + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_psscript_info.py b/ansible_collections/community/windows/plugins/modules/win_psscript_info.py new file mode 100644 index 000000000..7e4c4e335 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_psscript_info.py @@ -0,0 +1,206 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_psscript_info +short_description: Gather information about installed PowerShell Scripts +description: + - Gather information about PowerShell Scripts installed via PowerShellGet. +options: + name: + description: + - The name of the script. + - Supports any wildcard pattern supported by C(Get-InstalledScript). + - If omitted then all scripts will returned. + type: str + default: '*' + repository: + description: + - The name of the PSRepository the scripts were installed from. + - This acts as a filter against the scripts that would be returned based on the I(name) option. + - Only scripts installed from a registered repository will be returned. + - If the repository was re-registered after script installation with a new C(SourceLocation), this will not match. + type: str +requirements: + - C(PowerShellGet) module +seealso: + - module: community.windows.win_psrepository_info + - module: community.windows.win_psmodule_info +author: + - Brian Scholer (@briantist) +''' + +EXAMPLES = r''' +- name: Get info about all script on the system + community.windows.win_psscript_info: + +- name: Get info about the Test-RPC script + community.windows.win_psscript_info: + name: Test-RPC + +- name: Get info about test scripts + community.windows.win_psscript_info: + name: Test* + +- name: Get info about all scripts installed from the PSGallery repository + community.windows.win_psscript_info: + repository: PSGallery + register: gallery_scripts + +- name: Update all scripts retrieved from above example + community.windows.win_psscript: + name: "{{ item }}" + state: latest + loop: "{{ gallery_scripts.scripts | map(attribute=name) }}" + +- name: Get info about all scripts on the system + community.windows.win_psscript_info: + register: all_scripts + +- name: Find scripts installed from a repository that isn't registered now + set_fact: + missing_repository_scripts: "{{ + all_scripts + | json_query('scripts[?repository!=null && repository==repository_source_location].{name: name, version: version, repository: repository}') + | list + }}" + +- debug: + var: missing_repository_scripts +''' + +RETURN = r''' +scripts: + description: + - A list of installed scripts (or an empty list is there are none). + returned: always + type: list + elements: dict + contains: + name: + description: + - The name of the script. + type: str + sample: Test-RPC + version: + description: + - The script version. + type: str + sample: 1.2.3 + installed_location: + description: + - The path where the script is installed. + type: str + sample: 'C:\Program Files\WindowsPowerShell\Scripts' + author: + description: + - The author of the script. + type: str + sample: Ryan Ries + copyright: + description: + - The copyright of the script. + type: str + sample: 'Jordan Borean 2017' + company_name: + description: + - The company name of the script. + type: str + sample: Microsoft Corporation + description: + description: + - The description of the script. + type: str + sample: This scripts tests network connectivity. + dependencies: + description: + - The script's dependencies. + type: list + elements: str + icon_uri: + description: + - The address of the icon of the script. + type: str + sample: 'https://raw.githubusercontent.com/scripter/script/main/logo.png' + license_uri: + description: + - The address of the license for the script. + type: str + sample: 'https://raw.githubusercontent.com/scripter/script/main/LICENSE' + project_uri: + description: + - The address of the script's project. + type: str + sample: 'https://github.com/scripter/script' + repository_source_location: + description: + - The source location of the repository where the script was installed from. + type: str + sample: 'https://www.powershellgallery.com/api/v2' + repository: + description: + - The PSRepository where the script was installed from. + - This value is not historical. It depends on the PSRepositories that are registered now for the current user. + - The C(repository_source_location) must match the current source location of a registered repository to get a repository name. + - If there is no match, then this value will match C(repository_source_location). + type: str + sample: PSGallery + release_notes: + description: + - The script's release notes. This is a free text field and no specific format should be assumed. + type: str + sample: | + ## 1.5.5 + - Add optional param for detailed info + + ## 1.4.7 + - Bug fix for deadlock when getting parameters in an event + + ## 1.1.4 + - Bug fix when installing package from private feeds + installed_date: + description: + - The date the script was installed. + type: str + sample: '2018-02-14T17:55:34.9620740-05:00' + published_date: + description: + - The date the script was published. + type: str + sample: '2017-03-15T04:18:09.0000000' + updated_date: + description: + - The date the script was last updated. + type: str + sample: '2019-12-31T09:20:02.0000000' + package_management_provider: + description: + - This is the PowerShellGet package management provider used to install the script. + type: str + sample: NuGet + tags: + description: + - The tags defined in the script's C(AdditionalMetadata). + type: list + elements: str + sample: + - networking + - serialization + - git + - dsc + power_shell_get_format_version: + description: + - The version of the PowerShellGet specification format. + type: str + sample: '2.0' + additional_metadata: + description: + - Additional metadata included with the script or during publishing of the script. + - Many of the fields here are surfaced at the top level with some standardization. The values here may differ slightly as a result. + - The field names here vary widely in case, and are not normalized or converted to snake_case. + type: dict +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_pssession_configuration.ps1 b/ansible_collections/community/windows/plugins/modules/win_pssession_configuration.ps1 new file mode 100644 index 000000000..c73b3ed4f --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_pssession_configuration.ps1 @@ -0,0 +1,585 @@ +#!powershell + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic +#AnsibleRequires -CSharpUtil Ansible.AccessToken + +$type = @{ + guid = [Func[[Object], [System.Guid]]] { + [System.Guid]::ParseExact($args[0].Trim([char[]]'}{').Replace('-', ''), 'N') + } + version = [Func[[Object], [System.Version]]] { + [System.Version]::Parse($args[0]) + } + int64 = [Func[[Object], [System.Int64]]] { + [System.Int64]::Parse($args[0]) + } + double = [Func[[Object], [System.Double]]] { + [System.Double]::Parse($args[0]) + } +} + +$pssc_options = @{ + guid = @{ type = $type.guid } + schema_version = @{ type = $type.version } + author = @{ type = 'str' } + description = @{ type = 'str' } + company_name = @{ type = 'str' } + copyright = @{ type = 'str' } + session_type = @{ type = 'str' ; choices = @('default', 'empty', 'restricted_remote_server') } + transcript_directory = @{ type = 'path' } + run_as_virtual_account = @{ type = 'bool' } + run_as_virtual_account_groups = @{ type = 'list' ; elements = 'str' } + mount_user_drive = @{ type = 'bool' } + user_drive_maximum_size = @{ type = $type.int64 } + group_managed_service_account = @{ type = 'str' } + scripts_to_process = @{ type = 'list' ; elements = 'str' } + role_definitions = @{ type = 'dict' } + required_groups = @{ type = 'dict' } + language_mode = @{ type = 'str' ; choices = @('no_language', 'restricted_language', 'constrained_language', 'full_language') } + execution_policy = @{ type = 'str' ; choices = @('default', 'remote_signed', 'restricted', 'undefined', 'unrestricted') } + powershell_version = @{ type = $type.version } + modules_to_import = @{ type = 'list' ; elements = 'raw' } + visible_aliases = @{ type = 'list' ; elements = 'str' } + visible_cmdlets = @{ type = 'list' ; elements = 'raw' } + visible_functions = @{ type = 'list' ; elements = 'raw' } + visible_external_commands = @{ type = 'list' ; elements = 'str' } + alias_definitions = @{ type = 'dict' } + function_definitions = @{ type = 'dict' } + variable_definitions = @{ type = 'list' ; elements = 'dict' } + environment_variables = @{ type = 'dict' } + types_to_process = @{ type = 'list' ; elements = 'path' } + formats_to_process = @{ type = 'list' ; elements = 'path' } + assemblies_to_load = @{ type = 'list' ; elements = 'str' } +} + +$session_configuration_options = @{ + name = @{ type = 'str' ; required = $true } + processor_architecure = @{ type = 'str' ; choices = @('amd64', 'x86') } + access_mode = @{ type = 'str' ; choices = @('disabled', 'local', 'remote') } + use_shared_process = @{ type = 'bool' } + thread_apartment_state = @{ type = 'str' ; choices = @('mta', 'sta') } + thread_options = @{ type = 'str' ; choices = @('default', 'reuse_thread', 'use_current_thread', 'use_new_thread') } + startup_script = @{ type = 'path' } + maximum_received_data_size_per_command_mb = @{ type = $type.double } + maximum_received_object_size_mb = @{ type = $type.double } + security_descriptor_sddl = @{ type = 'str' } + run_as_credential_username = @{ type = 'str' } + run_as_credential_password = @{ type = 'str' ; no_log = $true } +} + +$behavior_options = @{ + state = @{ type = 'str' ; choices = @('present', 'absent') ; default = 'present' } + lenient_config_fields = @{ type = 'list' ; elements = 'str' ; default = @('guid', 'author', 'company_name', 'copyright', 'description') } + async_timeout = @{ type = 'int' ; default = 300 } + async_poll = @{ type = 'int' ; default = 1 } + <# + # TODO: possible future enhancement to wait for existing connections to finish + # Existing connections can be found with: + # Get-WSManInstance -ComputerName localhost -ResourceURI shell -Enumerate + + existing_connection_timeout_seconds = @{ type = 'int' ; default = 0 } + existing_connection_timeout_interval_ms = @{ type = 'int' ; default = 500 } + existing_connection_timeout_action = @{ type = 'str' ; choices = @('terminate', 'fail') ; default = 'terminate' } + existing_connection_wait_states = @{ type = 'list' ; elements = 'str' ; default = @('connected') } +#> +} + +$spec = @{ + options = $pssc_options + $session_configuration_options + $behavior_options + required_together = @( + , @('run_as_credential_username', 'run_as_credential_password') + ) + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + + +function Import-PowerShellDataFileLegacy { + <# + .SYNOPSIS + A pre-PowerShell 5.0 version of Import-PowerShellDataFile + + .DESCRIPTION + Safely imports a PowerShell Data file in PowerShell versions before 5.0 + when the built-in command was introduced. Non-literal Path support is not included. + #> + [CmdletBinding()] + [OutputType([hashtable])] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingInvokeExpression', '', Justification = 'Required to process PS data file')] + param( + [Parameter(Mandatory = $true)] + [Alias('Path')] + [String] + $LiteralPath + ) + + End { + $astloader = [System.Management.Automation.Language.Parser]::ParseFile($LiteralPath, [ref] $null , [ref] $null) + $ht = $astloader.Find({ param($ast) + $ast -is [System.Management.Automation.Language.HashtableAst] + }, $false) + + if (-not $ht) { + throw "Invalid PowerShell Data File." + } + + # SafeGetValue() is not available before PowerShell 5 anyway, so we'll do the unsafe load and just execute it. + # The only files we're loading are ones we generated from options, or ones that were already attached to existing + # session configurations. + # $ht.SafeGetValue() + Invoke-Expression -Command $ht.Extent.Text + } +} + +if (-not (Get-Command -Name 'Microsoft.PowerShell.Utility\Import-PowerShellDataFile' -ErrorAction SilentlyContinue)) { + New-Alias -Name 'Import-PowerShellDataFile' -Value 'Import-PowerShellDataFileLegacy' +} + +function ConvertFrom-SnakeCase { + [CmdletBinding()] + [OutputType([String])] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [String] + $SnakedString + ) + + Process { + [regex]::Replace($SnakedString, '^_?.|_.', { param($m) $m.Value.TrimStart('_').ToUpperInvariant() }) + } +} + +function ConvertFrom-AnsibleOption { + [CmdletBinding()] + [OutputType([System.Collections.IDictionary])] + param( + [Parameter(Mandatory = $true)] + [System.Collections.IDictionary] + $Params , + + [Parameter(Mandatory = $true)] + [hashtable] + $OptionSet + ) + + End { + $ret = @{} + foreach ($option in $OptionSet.GetEnumerator()) { + $raw_name = $option.Name + switch -Wildcard ($raw_name) { + 'run_as_credential_*' { + $raw_name = $raw_name -replace '_[^_]+$' + $name = ConvertFrom-SnakeCase -SnakedString $raw_name + if (-not $ret.Contains($name)) { + $un = $Params["${raw_name}_username"] + if ($un) { + $secpw = ConvertTo-SecureString -String $Params["${raw_name}_password"] -AsPlainText -Force + $value = New-Object -TypeName PSCredential -ArgumentList $un, $secpw + $ret[$name] = $value + } + } + break + } + + default { + $value = $Params[$raw_name] + if ($null -ne $value) { + if ($option.Value.choices) { + # the options that have choices have them listed in snake_case versions of their real values + $value = ConvertFrom-SnakeCase -SnakedString $value + } + $name = ConvertFrom-SnakeCase -SnakedString $raw_name + $ret[$name] = $value + } + } + } + } + + $ret + } +} + +function Write-GeneratedSessionConfiguration { + [CmdletBinding()] + [OutputType([System.IO.FileInfo])] + param( + [Parameter(Mandatory = $true)] + [System.Collections.IDictionary] + $ParameterSet , + + [Parameter()] + [String] + $OutFile + ) + + End { + $file = if ($OutFile) { + $OutFile + } + else { + [System.IO.Path]::GetTempFileName() + } + + $file = $file -replace '(?<!\.pssc)$', '.pssc' + New-PSSessionConfigurationFile -Path $file @ParameterSet + [System.IO.FileInfo]$file + } +} + +function Compare-ConfigFile { + <# + .SYNOPSIS + This function compares the existing config file to the desired + + .DESCRIPTION + We'll load the contents of both the desired and existing config, remove fields that shouldn't be + compared, then generate a new config based on the existing and compare those files. + + This could be done as a direct file compare, without loading the contents as objects. + The primary reasons to do it this slightly more complicated way are: + + - To ignore GUID as a value that matters: if you don't supply it a new one is generated for you, + but PSSessionConfigurations don't use this for anything; it's just metadata. If you supply one, + we want to compare it. If you don't, we shouldn't count the "mismatch" against you though. + + - To normalize the existing file based on the following stuff so we avoid unnecessary changes: + + - A file compare either has to be case insensitive (won't catch changes in values) or case sensitive + (will may force changes on differences that don't matter, like case differences in key values) + + - A file compare will see changes on whitespace and line ending differences; although those could be + accounted for in other ways, this method handles them. + + - A file compare will see changes on other non-impacting syntax style differences like indentation. + #> + [CmdletBinding()] + [OutputType([bool])] + param( + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [System.IO.FileInfo] + $ConfigFilePath , + + [Parameter(Mandatory = $true)] + [System.IO.FileInfo] + $NewConfigFile , + + [Parameter(Mandatory = $true)] + [System.Collections.IDictionary] + $Params , + + [Parameter()] + [String[]] + $UseExistingIfMissing + ) + + Process { + $desired_config = $NewConfigFile.FullName + + $existing_content = Import-PowerShellDataFile -LiteralPath $ConfigFilePath.FullName + $desired_content = Import-PowerShellDataFile -LiteralPath $desired_config + + $module.Diff.before.pscc = $existing_content + $module.Diff.after.pscc = $desired_content + + $regen = $false + foreach ($ignorable_param in $UseExistingIfMissing) { + # here we're checking for the parameters that shouldn't be compared if they are in the existing + # config, but missing in the desired config. To account for this, we copy the value from the + # existing into the desired so that when we regenerate it, it'll match the existing if there + # aren't other changes. + if (-not $Params.Contains($ignorable_param) -and $existing_content.Contains($ignorable_param)) { + $desired_content[$ignorable_param] = $existing_content[$ignorable_param] + $regen = $true + } + } + + # re-write and read the desired config file + if ($regen) { + $NewConfigFile.Delete() + $desired_config = Write-GeneratedSessionConfiguration -ParameterSet $desired_content -OutFile $desired_config + } + + $desired_content = Get-Content -Raw -LiteralPath $desired_config + + # re-write/import the existing one too to get a pristine version + # this will account for unimporant case differences, comments, whitespace, etc. + $pristine_config = Write-GeneratedSessionConfiguration -ParameterSet $existing_content + $existing_content = Get-Content -Raw -LiteralPath $pristine_config + + # with all this de/serializing out of the way we can just do a simple case-sensitive string compare + $desired_content -ceq $existing_content + + Remove-Item -LiteralPath $pristine_config -Force -ErrorAction SilentlyContinue + } +} + +function Compare-SessionOption { + <# + .DESCRIPTION + This function is used for comparing the session options that don't get set in the config file. + This _should_ have been straightforward for anything other than RunAsCredential, except that for + some godforesaken reason a smattering of settings have names that differ from their parameter name. + + This list is defined internally in PowerShell here: + https://git.io/JfUk7 + + #> + [CmdletBinding()] + [OutputType([bool])] + param( + [Parameter(Mandatory = $true)] + [System.Collections.IDictionary] + $DesiredOptions , + + [Parameter(Mandatory = $true)] + [Object] + $ExistingOptions + ) + + End { + $optnamer = @{ + ThreadApartmentState = 'pssessionthreadapartmentstate' + ThreadOptions = 'pssessionthreadoptions' + MaximumReceivedDataSizePerCommandMb = 'PSMaximumReceivedDataSizePerCommandMB' + MaximumReceivedObjectSizeMb = 'PSMaximumReceivedObjectSizeMB' + } | Add-Member -MemberType ScriptMethod -Name GetValueOrKey -Value { + param($key) + + $val = $this[$key] + if ($null -eq $val) { + return $key + } + else { + return $val + } + } -Force -PassThru + + # Determine diffs in diff mode + # AccessMode is not a property itself, it's a switch for the Register/Set command which changes some other config properties. + # Remove it from Diff because it cannot be determined... + $ignore_options_in_diff = @('AccessMode', 'RunAsCredential') + + $smatch = $true + $after = @{} + $before = @{} + + foreach ($opt in $session_configuration_options.GetEnumerator()) { + if ($opt.Name -in $ignore_options_in_diff) { continue } + $key = ConvertFrom-SnakeCase -SnakedString $optnamer.GetValueOrKey($opt.Name) + $before.Add($key, $ExistingOptions.($key)) + } + $after = $before.Clone() + + foreach ($opt in $DesiredOptions.GetEnumerator()) { + if ($opt.Name -in $ignore_options_in_diff) { continue } + $key = ConvertFrom-SnakeCase -SnakedString $optnamer.GetValueOrKey($opt.Name) + $after[$key] = $opt.Value + + $smatch = $smatch -and ( + $ExistingOptions.($optnamer.GetValueOrKey($opt.Name)) -ceq $opt.Value + ) + if (-not $smatch -and -not $module.DiffMode) { + break + } + } + + $module.Diff.before.session = $before + $module.Diff.after.session = $after + + if ($DesiredOptions.Contains('RunAsCredential')) { + # since we can't retrieve/compare password, a change must always be made if a cred is specified + return $false + } + + return $smatch + } +} + +<# + For use with possible future enhancement. + Right now the biggest challenges to this are: + - Ansible's connection itself: the number doesn't go to 0 while we're here running + and waiting for it. I thought being async it would disappear but either that's not + the case or it's taking too long to do so. + + - I have not found a reliable way to determine which WinRM connection is the one used for + the Ansible connection. Over psrp we can use Get-PSSession -ComputerName but that won't + work for the winrm connection plugin. + + - Connections seem to take time to disappear. In tests when trying to start time-limited + sessions, like: + icm -computername . -scriptblock { Start-Sleep -Seconds 30 } -AsJob + After the time elapses the connection lingers for a little while after. Should be ok but + does add some challenges to writing tests. + + - Checking for instances of the shell resource looks reliable, but I'm not yet certain + if it captures all WinRM connections, like CIM connections. Still would be better than + nothing. +#> +# function Wait-WinRMConnection { +# <# +# .SYNOPSIS +# Waits for existing WinRM connections to finish + +# .DESCRIPTION +# Finds existing WinRM connections that are in a set of states (configurable), and waits for them +# to disappear, or times out. +# #> +# [CmdletBinding()] +# param( +# [Parameter(Mandatory=$true)] +# [Ansible.Basic.AnsibleModule] +# $Module +# ) + +# End { +# $action = $Module.Params.existing_connection_timeout_action +# $states = $Module.Params.existing_connection_wait_states +# $timeout_ms = [System.Math]::Min(0, $Module.Params.existing_connection_timeout_seconds) * 1000 +# $interval = [System.Math]::Max([System.Math]::Min(100, $Module.Params.existing_connection_timeout_interval_ms), $timeout_ms) + +# # Would only with psrp +# $thiscon = Get-PSSession -ComputerName . | Select-Object -ExpandProperty InstanceId + +# $sw = New-Object -TypeName System.Diagnostics.Stopwatch + +# do { +# $connections = Get-WSManInstance -ComputerName localhost -ResourceURI shell -Enumerate | +# Where-Object -FilterScript { +# $states -contains $_.State -and ( +# -not $thiscon -or +# $thiscon -ne $_.ShellId +# ) +# } + +# $sw.Start() +# Start-Sleep -Milliseconds $interval +# } while ($connections -and $sw.ElapsedMilliseconds -lt $timeout_ms) +# $sw.Stop() + +# if ($connections -and $action -eq 'fail') { +# # somehow $connections.Count sometimes is blank (not 0) but I can't figure out how that's possible +# $Module.FailJson("$($connections.Count) remained after timeout.") +# } +# } +# } + +$PSDefaultParameterValues = @{ + '*-PSSessionConfiguration:Force' = $true + 'ConvertFrom-AnsibleOption:Params' = $module.Params + 'Wait-WinRMConnection:Module' = $module +} + +$opt_pssc = ConvertFrom-AnsibleOption -OptionSet $pssc_options +$opt_session = ConvertFrom-AnsibleOption -OptionSet $session_configuration_options + +$existing = Get-PSSessionConfiguration -Name $opt_session.Name -ErrorAction SilentlyContinue + +try { + $module.Diff.before = @{ session = $null; pscc = $null; } + $module.Diff.after = @{ session = $null; pscc = $null; } + + if ($opt_pssc.Count) { + # config file options were passed to the module, so generate a config file from those + $desired_config = Write-GeneratedSessionConfiguration -ParameterSet $opt_pssc + } + if ($existing) { + # the endpoint is registered + if ($existing.ConfigFilePath -and (Test-Path -LiteralPath $existing.ConfigFilePath)) { + # the registered endpoint uses a config file + if ($desired_config) { + # a desired config file exists, so compare it to the existing one + $content_match = $existing | + Compare-ConfigFile -NewConfigFile $desired_config -Params $opt_pssc -UseExistingIfMissing ( + $module.Params.lenient_config_fields | ConvertFrom-SnakeCase + ) + } + else { + # existing endpoint has a config file but no config file options were passed, so there is no match + $content_match = $false + } + } + else { + # existing endpoint doesn't use a config file, so it's a match if there are also no config options passed + $content_match = $opt_pssc.Count -eq 0 + } + + $session_match = Compare-SessionOption -DesiredOptions $opt_session -ExistingOptions $existing + } + + $state = $module.Params.state + + $create = $state -eq 'present' -and (-not $existing -or -not $content_match) + $remove = $existing -and ($state -eq 'absent' -or -not $content_match) + $session_change = -not $session_match -and $state -ne 'absent' + + $module.Result.changed = $create -or $remove -or $session_change + + # In this module, we pre-emptively remove the session configuratin if there's any change + # in the config file options, and then re-register later if needed. + # But if the RunAs credential is wrong, the register will fail, and since we already removed + # the existing one, it will be gone. + # + # So let's ensure we can actually use the credential by logging on with TokenUtil, + # that way we can fail before touching the existing config. + if ($opt_session.Contains('RunAsCredential')) { + $cred = $opt_session.RunAsCredential + $username = $cred.Username + $domain = $null + if ($username.Contains('\')) { + $domain, $username = $username.Split('\') + } + try { + $handle = [Ansible.AccessToken.TokenUtil]::LogonUser($username, $domain, $cred.GetNetworkCredential().Password, 'Network', 'Default') + $handle.Dispose() + } + catch { + $module.FailJson("Could not validate RunAs Credential: $($_.Exception.Message)", $_) + } + } + + if (-not $module.CheckMode) { + if ($remove) { + # Wait-WinRMConnection + $null = Unregister-PSSessionConfiguration -Name $opt_session.Name + } + + if ($create) { + if ($desired_config) { + $opt_session.Path = $desired_config + } + # Wait-WinRMConnection + $null = Register-PSSessionConfiguration @opt_session + } + elseif ($session_change) { + $psso = $opt_session + # Wait-WinRMConnection + $null = Set-PSSessionConfiguration @psso + } + } +} +catch [System.Management.Automation.ParameterBindingException] { + $e = $_ + if ($e.Exception.ErrorId -eq 'NamedParameterNotFound') { + $psv = $PSVersionTable.PSVersion.ToString(2) + $param = $e.Exception.ParameterName + $cmd = $e.InvocationInfo.MyCommand.Name + $message = "Parameter '$param' is not available for '$cmd' in PowerShell $psv." + } + else { + $message = "Unknown parameter binding error: $($e.Exception.Message)" + } + + $module.FailJson($message, $e) +} +finally { + if ($desired_config) { + Remove-Item -LiteralPath $desired_config -Force -ErrorAction SilentlyContinue + } +} + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_pssession_configuration.py b/ansible_collections/community/windows/plugins/modules/win_pssession_configuration.py new file mode 100644 index 000000000..d6beb9ab6 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_pssession_configuration.py @@ -0,0 +1,390 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_pssession_configuration +short_description: Manage PSSession Configurations +description: + - Register, unregister, and modify PSSession Configurations for PowerShell remoting. +options: + name: + description: + - The name of the session configuration to manage. + type: str + required: yes + state: + description: + - The desired state of the configuration. + type: str + choices: + - present + - absent + default: present + guid: + description: + - The GUID (UUID) of the session configuration file. + - This value is metadata, so it only matters if you use it externally. + - If not set, a value will be generated automatically. + - Acceptable GUID formats are flexible. Any string of 32 hexadecimal digits will be accepted, with all hyphens C(-) and opening/closing C({}) ignored. + - See also I(lenient_config_fields). + type: raw + schema_version: + description: + - The schema version of the session configuration file. + - If not set, a value will be generated automatically. + - Must be a valid .Net System.Version string. + type: raw + author: + description: + - The author of the session configuration. + - This value is metadata and does not affect the functionality of the session configuration. + - If not set, a value may be generated automatically. + - See also I(lenient_config_fields). + type: str + description: + description: + - The description of the session configuration. + - This value is metadata and does not affect the functionality of the session configuration. + - See also I(lenient_config_fields). + type: str + company_name: + description: + - The company that authored the session configuration. + - This value is metadata and does not affect the functionality of the session configuration. + - If not set, a value may be generated automatically. + - See also I(lenient_config_fields). + type: str + copyright: + description: + - The copyright statement of the session configuration. + - This value is metadata and does not affect the functionality of the session configuration. + - If not set, a value may be generated automatically. + - See also I(lenient_config_fields). + type: str + session_type: + description: + - Controls what type of session this is. + type: str + choices: + - default + - empty + - restricted_remote_server + transcript_directory: + description: + - Automatic session transcripts will be written to this directory. + type: path + run_as_virtual_account: + description: + - If C(yes) the session runs as a virtual account. + - Do not use I(run_as_credential_username) and I(run_as_credential_password) to specify a virtual account. + type: bool + run_as_virtual_account_groups: + description: + - If I(run_as_virtual_account=yes) this is a list of groups to add the virtual account to. + type: list + elements: str + mount_user_drive: + description: + - If C(yes) the session creates and mounts a user-specific PSDrive for use with file transfers. + type: bool + user_drive_maximum_size: + description: + - The maximum size of the user drive in bytes. + - Must fit into an Int64. + type: raw + group_managed_service_account: + description: + - If the session will run as a group managed service account (gMSA) then this is the name. + - Do not use I(run_as_credential_username) and I(run_as_credential_password) to specify a gMSA. + type: str + scripts_to_process: + description: + - A list of paths to script files ending in C(.ps1) that should be applied to the session. + type: list + elements: str + role_definitions: + description: + - A dict defining the roles for JEA sessions. + - For more information see U(https://docs.microsoft.com/en-us/powershell/scripting/learn/remoting/jea/session-configurations#role-definitions). + type: dict + required_groups: + description: + - For JEA sessions, defines conditional access rules about which groups a connecting user must belong to. + - For more information see U(https://docs.microsoft.com/en-us/powershell/scripting/learn/remoting/jea/session-configurations#conditional-access-rules). + type: dict + language_mode: + description: + - Determines the language mode of the PowerShell session. + type: str + choices: + - no_language + - restricted_language + - constrained_language + - full_language + execution_policy: + description: + - The execution policy controlling script execution in the PowerShell session. + type: str + choices: + - default + - remote_signed + - restricted + - undefined + - unrestricted + powershell_version: + description: + - The minimum required PowerShell version for this session. + - Must be a valid .Net System.Version string. + type: raw + modules_to_import: + description: + - A list of modules that should be imported into the session. + - Any valid PowerShell module spec can be used here, so simple str names or dicts can be used. + - If a dict is used, no snake_case conversion is done, so the original PowerShell names must be used. + type: list + elements: raw + visible_aliases: + description: + - The aliases that can be used in the session. + - For more information see U(https://docs.microsoft.com/en-us/powershell/scripting/learn/remoting/jea/role-capabilities). + type: list + elements: str + visible_cmdlets: + description: + - The cmdlets that can be used in the session. + - The elements can be simple names or complex command specifications. + - For more information see U(https://docs.microsoft.com/en-us/powershell/scripting/learn/remoting/jea/role-capabilities). + type: list + elements: raw + visible_functions: + description: + - The functions that can be used in the session. + - The elements can be simple names or complex command specifications. + - For more information see U(https://docs.microsoft.com/en-us/powershell/scripting/learn/remoting/jea/role-capabilities). + type: list + elements: raw + visible_external_commands: + description: + - The external commands and scripts that can be used in the session. + - For more information see U(https://docs.microsoft.com/en-us/powershell/scripting/learn/remoting/jea/role-capabilities). + type: list + elements: str + alias_definitions: + description: + - A dict that defines aliases for each session. + type: dict + function_definitions: + description: + - A dict that defines functions for each session. + type: dict + variable_definitions: + description: + - A list of dicts where each elements defines a variable for each session. + type: list + elements: dict + environment_variables: + description: + - A dict that defines environment variables for each session. + type: dict + types_to_process: + description: + - Paths to type definition files to process for each session. + type: list + elements: path + formats_to_process: + description: + - Paths to format definition files to process for each session. + type: list + elements: path + assemblies_to_load: + description: + - The assemblies that should be loaded into each session. + type: list + elements: str + processor_architecure: + description: + - The processor architecture of the session (32 bit vs. 64 bit). + type: str + choices: + - amd64 + - x86 + access_mode: + description: + - Controls whether the session configuration allows connection from the C(local) machine only, both local and C(remote), or none (C(disabled)). + type: str + choices: + - disabled + - local + - remote + use_shared_process: + description: + - If C(yes) then the session shares a process for each session. + type: bool + thread_apartment_state: + description: + - The apartment state for the PowerShell session. + type: str + choices: + - mta + - sta + thread_options: + description: + - Sets thread options for the session. + type: str + choices: + - default + - reuse_thread + - use_current_thread + - use_new_thread + startup_script: + description: + - A script that gets run on session startup. + type: path + maximum_received_data_size_per_command_mb: + description: + - Sets the maximum received data size per command in MB. + - Must fit into a double precision floating point value. + type: raw + maximum_received_object_size_mb: + description: + - Sets the maximum object size in MB. + - Must fit into a double precision floating point value. + type: raw + security_descriptor_sddl: + description: + - An SDDL string that controls which users and groups can connect to the session. + - If I(role_definitions) is specified the security descriptor will be set based on that. + - If this option is not specified the default security descriptor will be applied. + type: str + run_as_credential_username: + description: + - Used to set a RunAs account for the session. All commands executed in the session will be run as this user. + - To use a gMSA, see I(group_managed_service_account). + - To use a virtual account, see I(run_as_virtual_account) and I(run_as_virtual_account_groups). + - Status will always be C(changed) when a RunAs credential is set because the password cannot be retrieved for comparison. + type: str + run_as_credential_password: + description: + - The password for I(run_as_credential_username). + type: str + lenient_config_fields: + description: + - Some fields used in the session configuration do not affect its function, and are sometimes auto-generated when not specified. + - To avoid unnecessarily changing the configuration on each run, the values of these options will only be enforced when they are explicitly specified. + type: list + elements: str + default: + - guid + - author + - company_name + - copyright + - description + async_timeout: + description: + - Sets a timeout for how long in seconds to wait for asynchronous module execution and waiting for the connection to recover. + - Replicates the functionality of the C(async) keyword. + - Has no effect in check mode. + type: int + default: 300 + async_poll: + description: + - Sets a delay in seconds between each check of the asynchronous execution status. + - Replicates the functionality of the C(poll) keyword. + - Has no effect in check mode. + - I(async_poll=0) is not supported. + type: int + default: 1 +notes: + - This module will restart the WinRM service on any change. This will terminate all WinRM connections including those by other Ansible runs. + - Internally this module uses C(async) when not in check mode to ensure things go smoothly when restarting the WinRM service. + - The standard C(async) and C(poll) keywords cannot be used; instead use the I(async_timeout) and I(async_poll) options to control asynchronous execution. + - Options that don't list a default value here will use the defaults of C(New-PSSessionConfigurationFile) and C(Register-PSSessionConfiguration). + - If a value can be specified in both a session config file and directly in the session options, this module will prefer the setting be in the config file. +seealso: + - name: C(New-PSSessionConfigurationFile) Reference + description: Details and defaults for options that end up in the session configuration file. + link: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/new-pssessionconfigurationfile + - name: C(Register-PSSessionConfiguration) Reference + description: Details and defaults for options that are not specified in the session config file. + link: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/register-pssessionconfiguration + - name: PowerShell Just Enough Administration (JEA) + description: Refer to the JEA documentation for advanced usage of some options + link: https://docs.microsoft.com/en-us/powershell/scripting/learn/remoting/jea/overview + - name: About Session Configurations + description: General information about session configurations. + link: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_session_configurations + - name: About Session Configuration Files + description: General information about session configuration files. + link: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_session_configuration_files +author: + - Brian Scholer (@briantist) +''' + +EXAMPLES = r''' +- name: Register a session configuration that loads modules automatically + community.windows.win_pssession_configuration: + name: WebAdmin + modules_to_import: + - WebAdministration + - IISAdministration + description: This endpoint has IIS modules pre-loaded + +- name: Set up an admin endpoint with a restricted execution policy + community.windows.win_pssession_configuration: + name: GloboCorp.Admin + company_name: Globo Corp + description: Admin Endpoint + execution_policy: restricted + +- name: Create a complex JEA endpoint + community.windows.win_pssession_configuration: + name: RBAC.Endpoint + session_type: restricted_remote_server + run_as_virtual_account: True + transcript_directory: '\\server\share\Transcripts' + language_mode: no_language + execution_policy: restricted + role_definitions: + 'CORP\IT Support': + RoleCapabilities: + - PasswordResetter + - EmployeeOffboarder + 'CORP\Webhosts': + RoleCapabilities: IISAdmin + visible_functions: + - tabexpansion2 + - help + visible_cmdlets: + - Get-Help + - Name: Get-Service + Parameters: + - Name: DependentServices + - Name: RequiredServices + - Name: Name + ValidateSet: + - WinRM + - W3SVC + - WAS + visible_aliases: + - gsv + state: present + +- name: Remove a session configuration + community.windows.win_pssession_configuration: + name: UnusedEndpoint + state: absent + +- name: Set a sessions configuration with tweaked async values + community.windows.win_pssession_configuration: + name: MySession + description: A sample session + async_timeout: 500 + async_poll: 5 +''' + +RETURN = r''' +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_rabbitmq_plugin.ps1 b/ansible_collections/community/windows/plugins/modules/win_rabbitmq_plugin.ps1 new file mode 100644 index 000000000..f4ac711c6 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_rabbitmq_plugin.ps1 @@ -0,0 +1,156 @@ +#!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 + +function Get-EnabledPlugin($rabbitmq_plugins_cmd) { + $list_plugins_cmd = "$rabbitmq_plugins_cmd list -E -m" + try { + $enabled_plugins = @(Invoke-Expression "& $list_plugins_cmd" | Where-Object { $_ }) + return , $enabled_plugins + } + catch { + Fail-Json -obj $result -message "Can't execute `"$($list_plugins_cmd)`": $($_.Exception.Message)" + } +} + +function Enable-Plugin($rabbitmq_plugins_cmd, $plugin_name) { + $enable_plugin_cmd = "$rabbitmq_plugins_cmd enable $plugin_name" + try { + Invoke-Expression "& $enable_plugin_cmd" + } + catch { + Fail-Json -obj $result -message "Can't execute `"$($enable_plugin_cmd)`": $($_.Exception.Message)" + } +} + +function Disable-Plugin($rabbitmq_plugins_cmd, $plugin_name) { + $enable_plugin_cmd = "$rabbitmq_plugins_cmd disable $plugin_name" + try { + Invoke-Expression "& $enable_plugin_cmd" + } + catch { + Fail-Json -obj $result -message "Can't execute `"$($enable_plugin_cmd)`": $($_.Exception.Message)" + } +} + +function Get-RabbitmqPathFromRegistry { + $reg64Path = "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\RabbitMQ" + $reg32Path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\RabbitMQ" + + if (Test-Path -LiteralPath $reg64Path) { + $regPath = $reg64Path + } + elseif (Test-Path -LiteralPath $reg32Path) { + $regPath = $reg32Path + } + + if ($regPath) { + $path = Split-Path -Parent (Get-ItemProperty -LiteralPath $regPath "UninstallString").UninstallString + $version = (Get-ItemProperty -LiteralPath $regPath "DisplayVersion").DisplayVersion + return "$path\rabbitmq_server-$version" + } +} + +function Get-RabbitmqBinPath($installation_path) { + $result = Join-Path -Path $installation_path -ChildPath 'bin' + if (Test-Path -LiteralPath $result) { + return $result + } + + $result = Join-Path -Path $installation_path -ChildPath 'sbin' + if (Test-Path -LiteralPath $result) { + return $result + } +} + +$ErrorActionPreference = "Stop" + +$result = @{ + changed = $false + enabled = @() + disabled = @() +} + +$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 + +$names = Get-AnsibleParam -obj $params -name "names" -type "str" -failifempty $true -aliases "name" +$new_only = Get-AnsibleParam -obj $params -name "new_only" -type "bool" -default $false +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "enabled" -validateset "enabled", "disabled" +$prefix = Get-AnsibleParam -obj $params -name "prefix" -type "str" + +if ($diff_support) { + $result.diff = @{} + $result.diff.prepared = "" +} + +$plugins = $names.Split(",") + +if ($prefix) { + $rabbitmq_bin_path = Get-RabbitmqBinPath -installation_path $prefix + if (-not $rabbitmq_bin_path) { + Fail-Json -obj $result -message "No binary folder in prefix `"$($prefix)`"" + } +} +else { + $rabbitmq_reg_path = Get-RabbitmqPathFromRegistry + if ($rabbitmq_reg_path) { + $rabbitmq_bin_path = Get-RabbitmqBinPath -installation_path $rabbitmq_reg_path + } +} + +if ($rabbitmq_bin_path) { + $rabbitmq_plugins_cmd = "'$(Join-Path -Path $rabbitmq_bin_path -ChildPath "rabbitmq-plugins")'" +} +else { + $rabbitmq_plugins_cmd = "rabbitmq-plugins" +} + +$enabled_plugins = Get-EnabledPlugin -rabbitmq_plugins_cmd $rabbitmq_plugins_cmd + +if ($state -eq "enabled") { + $plugins_to_enable = $plugins | Where-Object { -not ($enabled_plugins -contains $_) } + foreach ($plugin in $plugins_to_enable) { + if (-not $check_mode) { + Enable-Plugin -rabbitmq_plugins_cmd $rabbitmq_plugins_cmd -plugin_name $plugin + } + if ($diff_support) { + $result.diff.prepared += "+[$plugin]`n" + } + $result.enabled += $plugin + $result.changed = $true + } + + if (-not $new_only) { + $plugins_to_disable = $enabled_plugins | Where-Object { -not ($plugins -contains $_) } + foreach ($plugin in $plugins_to_disable) { + if (-not $check_mode) { + Disable-Plugin -rabbitmq_plugins_cmd $rabbitmq_plugins_cmd -plugin_name $plugin + } + if ($diff_support) { + $result.diff.prepared += "-[$plugin]`n" + } + $result.disabled += $plugin + $result.changed = $true + } + } +} +else { + $plugins_to_disable = $enabled_plugins | Where-Object { $plugins -contains $_ } + foreach ($plugin in $plugins_to_disable) { + if (-not $check_mode) { + Disable-Plugin -rabbitmq_plugins_cmd $rabbitmq_plugins_cmd -plugin_name $plugin + } + if ($diff_support) { + $result.diff.prepared += "-[$plugin]`n" + } + $result.disabled += $plugin + $result.changed = $true + } +} + +Exit-Json $result diff --git a/ansible_collections/community/windows/plugins/modules/win_rabbitmq_plugin.py b/ansible_collections/community/windows/plugins/modules/win_rabbitmq_plugin.py new file mode 100644 index 000000000..c981c8d9f --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_rabbitmq_plugin.py @@ -0,0 +1,58 @@ +#!/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) + +DOCUMENTATION = r''' +--- +module: win_rabbitmq_plugin +short_description: Manage RabbitMQ plugins +description: + - Manage RabbitMQ plugins. +options: + names: + description: + - Comma-separated list of plugin names. + type: str + required: yes + aliases: [ name ] + new_only: + description: + - Only enable missing plugins. + - Does not disable plugins that are not in the names list. + type: bool + default: no + state: + description: + - Specify if plugins are to be enabled or disabled. + type: str + choices: [ disabled, enabled ] + default: enabled + prefix: + description: + - Specify a custom install prefix to a Rabbit. + type: str +author: + - Artem Zinenko (@ar7z1) +''' + +EXAMPLES = r''' +- name: Enables the rabbitmq_management plugin + community.windows.win_rabbitmq_plugin: + names: rabbitmq_management + state: enabled +''' + +RETURN = r''' +enabled: + description: List of plugins enabled during task run. + returned: always + type: list + sample: ["rabbitmq_management"] +disabled: + description: List of plugins disabled during task run. + returned: always + type: list + sample: ["rabbitmq_management"] +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_rds_cap.ps1 b/ansible_collections/community/windows/plugins/modules/win_rds_cap.ps1 new file mode 100644 index 000000000..4aa6516ba --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_rds_cap.ps1 @@ -0,0 +1,407 @@ +#!powershell + +# Copyright: (c) 2018, Kevin Subileau (@ksubileau) +# 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" + +# List of authentication methods as string. Used for parameter validation and conversion to integer flag, so order is important! +$auth_methods_set = @("none", "password", "smartcard", "both") +# List of session timeout actions as string. Used for parameter validation and conversion to integer flag, so order is important! +$session_timeout_actions_set = @("disconnect", "reauth") + +$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 + +$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent", "present", "enabled", "disabled" +$auth_method = Get-AnsibleParam -obj $params -name "auth_method" -type "str" -validateset $auth_methods_set +$order = Get-AnsibleParam -obj $params -name "order" -type "int" +$session_timeout = Get-AnsibleParam -obj $params -name "session_timeout" -type "int" +$session_timeout_action_params = @{ + obj = $params + name = "session_timeout_action" + type = "str" + default = "disconnect" + validateset = $session_timeout_actions_set +} +$session_timeout_action = Get-AnsibleParam @session_timeout_action_params +$idle_timeout = Get-AnsibleParam -obj $params -name "idle_timeout" -type "int" +$allow_only_sdrts_servers = Get-AnsibleParam -obj $params -name "allow_only_sdrts_servers" -type "bool" +$user_groups = Get-AnsibleParam -obj $params -name "user_groups" -type "list" +$computer_groups = Get-AnsibleParam -obj $params -name "computer_groups" -type "list" + +# Device redirections +$redirect_clipboard = Get-AnsibleParam -obj $params -name "redirect_clipboard" -type "bool" +$redirect_drives = Get-AnsibleParam -obj $params -name "redirect_drives" -type "bool" +$redirect_printers = Get-AnsibleParam -obj $params -name "redirect_printers" -type "bool" +$redirect_serial = Get-AnsibleParam -obj $params -name "redirect_serial" -type "bool" +$redirect_pnp = Get-AnsibleParam -obj $params -name "redirect_pnp" -type "bool" + + +Function ConvertTo-Sid { + [OutputType([string])] + [CmdletBinding()] + param ( + [Parameter(Mandatory, ValueFromPipeline)] + [string[]] + $InputObject + ) + + process { + foreach ($user in $InputObject) { + # RDS uses the UPN format with the builtin domain but Convert-ToSID tries to look this up as a domain. + # Ensure the input value is in the Netlogon format to ensure BUILTIN is resolved properly + if ($user.EndsWith("@builtin", [System.StringComparison]::OrdinalIgnoreCase)) { + $user = "BUILTIN\$($user.Substring(0, $user.Length - 8))" + } + + Convert-ToSID -account_name $user + } + } +} + + +function Get-CAP([string] $name) { + $cap_path = "RDS:\GatewayServer\CAP\$name" + $cap = @{ + Name = $name + } + + # Fetch CAP properties + Get-ChildItem -LiteralPath $cap_path | ForEach-Object { $cap.Add($_.Name, $_.CurrentValue) } + # Convert boolean values + $cap.Enabled = $cap.Status -eq 1 + $cap.Remove("Status") + $cap.AllowOnlySDRTSServers = $cap.AllowOnlySDRTSServers -eq 1 + + # Convert multiple choices values + $cap.AuthMethod = $auth_methods_set[$cap.AuthMethod] + $cap.SessionTimeoutAction = $session_timeout_actions_set[$cap.SessionTimeoutAction] + + # Fetch CAP device redirection settings + $cap.DeviceRedirection = @{} + Get-ChildItem -LiteralPath "$cap_path\DeviceRedirection" | ForEach-Object { $cap.DeviceRedirection.Add($_.Name, ($_.CurrentValue -eq 1)) } + + # Fetch CAP user and computer groups in Down-Level Logon format + $cap.UserGroups = @( + Get-ChildItem -LiteralPath "$cap_path\UserGroups" | + Select-Object -ExpandProperty Name | + ForEach-Object { Convert-FromSID -sid (ConvertTo-Sid -InputObject $_) } + ) + $cap.ComputerGroups = @( + Get-ChildItem -LiteralPath "$cap_path\ComputerGroups" | + Select-Object -ExpandProperty Name | + ForEach-Object { Convert-FromSID -sid (ConvertTo-Sid -InputObject $_) } + ) + + return $cap +} + +function Set-CAPPropertyValue { + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [Parameter(Mandatory = $true)] + [string] $name, + [Parameter(Mandatory = $true)] + [string] $property, + [Parameter(Mandatory = $true)] + $value, + [Parameter()] + $resultobj = @{} + ) + + $cap_path = "RDS:\GatewayServer\CAP\$name" + + try { + Set-Item -LiteralPath "$cap_path\$property" -Value $value -ErrorAction Stop + } + catch { + Fail-Json -obj $resultobj -message "Failed to set property $property of CAP ${name}: $($_.Exception.Message)" + } +} + +$result = @{ + changed = $false +} +$diff_text = $null + +# Validate CAP name +if ($name -match "[*/\\;:?`"<>|\t]+") { + Fail-Json -obj $result -message "Invalid character in CAP name." +} + +# Validate user groups +if ($null -ne $user_groups) { + if ($user_groups.Count -lt 1) { + Fail-Json -obj $result -message "Parameter 'user_groups' cannot be an empty list." + } + + $user_groups = $user_groups | ForEach-Object { + $group = $_ + # Test that the group is resolvable on the local machine + $sid = ConvertTo-Sid -InputObject $group + if (!$sid) { + Fail-Json -obj $result -message "$group is not a valid user group on the host machine or domain" + } + + # Return the normalized group name in Down-Level Logon format + Convert-FromSID -sid $sid + } + $user_groups = @($user_groups) +} + +# Validate computer groups +if ($null -ne $computer_groups) { + $computer_groups = $computer_groups | ForEach-Object { + $group = $_ + # Test that the group is resolvable on the local machine + $sid = ConvertTo-Sid -InputObject $group + if (!$sid) { + Fail-Json -obj $result -message "$group is not a valid computer group on the host machine or domain" + } + + # Return the normalized group name in Down-Level Logon format + Convert-FromSID -sid $sid + } + $computer_groups = @($computer_groups) +} + +# Validate order parameter +if ($null -ne $order -and $order -lt 1) { + Fail-Json -obj $result -message "Parameter 'order' must be a strictly positive integer." +} + +# Ensure RemoteDesktopServices module is loaded +if ($null -eq (Get-Module -Name RemoteDesktopServices -ErrorAction SilentlyContinue)) { + Import-Module -Name RemoteDesktopServices +} + +# Check if a CAP with the given name already exists +$cap_exist = Test-Path -LiteralPath "RDS:\GatewayServer\CAP\$name" + +if ($state -eq 'absent') { + if ($cap_exist) { + Remove-Item -LiteralPath "RDS:\GatewayServer\CAP\$name" -Recurse -WhatIf:$check_mode + $diff_text += "-[$name]" + $result.changed = $true + } +} +else { + $diff_text_added_prefix = '' + if (-not $cap_exist) { + if ($null -eq $user_groups) { + Fail-Json -obj $result -message "User groups must be defined to create a new CAP." + } + + # Auth method is required when creating a new CAP. Set it to password by default. + if ($null -eq $auth_method) { + $auth_method = "password" + } + + # Create a new CAP + if (-not $check_mode) { + $CapArgs = @{ + Name = $name + UserGroupNames = $user_groups -join ';' + } + $cimParams = @{ + Namespace = "Root\CIMV2\TerminalServices" + ClassName = "Win32_TSGatewayConnectionAuthorizationPolicy" + MethodName = "Create" + Arguments = $CapArgs + } + $return = Invoke-CimMethod @cimParams + if ($return.ReturnValue -ne 0) { + Fail-Json -obj $result -message "Failed to create CAP $name (code: $($return.ReturnValue))" + } + } + + $cap_exist = -not $check_mode + + $diff_text_added_prefix = '+' + $result.changed = $true + } + + $diff_text += "$diff_text_added_prefix[$name]`n" + + # We cannot configure a CAP that was created above in check mode as it won't actually exist + if ($cap_exist) { + $cap = Get-CAP -Name $name + $wmi_cap = Get-CimInstance -ClassName Win32_TSGatewayConnectionAuthorizationPolicy -Namespace Root\CIMv2\TerminalServices -Filter "name='$($name)'" + + if ($state -in @('disabled', 'enabled')) { + $cap_enabled = $state -ne 'disabled' + if ($cap.Enabled -ne $cap_enabled) { + $diff_text += "-State = $(@('disabled', 'enabled')[[int]$cap.Enabled])`n+State = $state`n" + Set-CAPPropertyValue -Name $name -Property Status -Value ([int]$cap_enabled) -ResultObj $result -WhatIf:$check_mode + $result.changed = $true + } + } + + if ($null -ne $auth_method -and $auth_method -ne $cap.AuthMethod) { + $diff_text += "-AuthMethod = $($cap.AuthMethod)`n+AuthMethod = $auth_method`n" + $set_params = @{ + Name = $name + Property = "AuthMethod" + Value = ([array]::IndexOf($auth_methods_set, $auth_method)) + ResultObj = $result + WhatIf = $check_mode + } + Set-CAPPropertyValue @set_params + $result.changed = $true + } + + if ($null -ne $order -and $order -ne $cap.EvaluationOrder) { + # Order cannot be greater than the total number of existing CAPs (InvalidArgument exception) + $cap_count = (Get-ChildItem -LiteralPath "RDS:\GatewayServer\CAP").Count + if ($order -gt $cap_count) { + $msg = -join @( + "Given value '$order' for parameter 'order' is greater than the number of existing CAPs. " + "The actual order will be capped to '$cap_count'." + ) + Add-Warning -obj $result -message $msg + $order = $cap_count + } + + $diff_text += "-Order = $($cap.EvaluationOrder)`n+Order = $order`n" + Set-CAPPropertyValue -Name $name -Property EvaluationOrder -Value $order -ResultObj $result -WhatIf:$check_mode + $result.changed = $true + } + + if ($null -ne $session_timeout -and ($session_timeout -ne $cap.SessionTimeout -or $session_timeout_action -ne $cap.SessionTimeoutAction)) { + try { + Set-Item -Path "RDS:\GatewayServer\CAP\$name\SessionTimeout" ` + -Value $session_timeout ` + -SessionTimeoutAction ([array]::IndexOf($session_timeout_actions_set, $session_timeout_action)) ` + -ErrorAction Stop ` + -WhatIf:$check_mode + } + catch { + Fail-Json -obj $result -message "Failed to set property ComputerGroupType of RAP ${name}: $($_.Exception.Message)" + } + + $diff_text += "-SessionTimeoutAction = $($cap.SessionTimeoutAction)`n+SessionTimeoutAction = $session_timeout_action`n" + $diff_text += "-SessionTimeout = $($cap.SessionTimeout)`n+SessionTimeout = $session_timeout`n" + $result.changed = $true + } + + if ($null -ne $idle_timeout -and $idle_timeout -ne $cap.IdleTimeout) { + $diff_text += "-IdleTimeout = $($cap.IdleTimeout)`n+IdleTimeout = $idle_timeout`n" + Set-CAPPropertyValue -Name $name -Property IdleTimeout -Value $idle_timeout -ResultObj $result -WhatIf:$check_mode + $result.changed = $true + } + + if ($null -ne $allow_only_sdrts_servers -and $allow_only_sdrts_servers -ne $cap.AllowOnlySDRTSServers) { + $diff_text += "-AllowOnlySDRTSServers = $($cap.AllowOnlySDRTSServers)`n+AllowOnlySDRTSServers = $allow_only_sdrts_servers`n" + Set-CAPPropertyValue -Name $name -Property AllowOnlySDRTSServers -Value ([int]$allow_only_sdrts_servers) -ResultObj $result -WhatIf:$check_mode + $result.changed = $true + } + + if ($null -ne $redirect_clipboard -and $redirect_clipboard -ne $cap.DeviceRedirection.Clipboard) { + $diff_text += "-RedirectClipboard = $($cap.DeviceRedirection.Clipboard)`n+RedirectClipboard = $redirect_clipboard`n" + Set-CAPPropertyValue -Name $name -Property "DeviceRedirection\Clipboard" -Value ([int]$redirect_clipboard) -ResultObj $result -WhatIf:$check_mode + $result.changed = $true + } + + if ($null -ne $redirect_drives -and $redirect_drives -ne $cap.DeviceRedirection.DiskDrives) { + $diff_text += "-RedirectDrives = $($cap.DeviceRedirection.DiskDrives)`n+RedirectDrives = $redirect_drives`n" + Set-CAPPropertyValue -Name $name -Property "DeviceRedirection\DiskDrives" -Value ([int]$redirect_drives) -ResultObj $result -WhatIf:$check_mode + $result.changed = $true + } + + if ($null -ne $redirect_printers -and $redirect_printers -ne $cap.DeviceRedirection.Printers) { + $diff_text += "-RedirectPrinters = $($cap.DeviceRedirection.Printers)`n+RedirectPrinters = $redirect_printers`n" + Set-CAPPropertyValue -Name $name -Property "DeviceRedirection\Printers" -Value ([int]$redirect_printers) -ResultObj $result -WhatIf:$check_mode + $result.changed = $true + } + + if ($null -ne $redirect_serial -and $redirect_serial -ne $cap.DeviceRedirection.SerialPorts) { + $diff_text += "-RedirectSerial = $($cap.DeviceRedirection.SerialPorts)`n+RedirectSerial = $redirect_serial`n" + Set-CAPPropertyValue -Name $name -Property "DeviceRedirection\SerialPorts" -Value ([int]$redirect_serial) -ResultObj $result -WhatIf:$check_mode + $result.changed = $true + } + + if ($null -ne $redirect_pnp -and $redirect_pnp -ne $cap.DeviceRedirection.PlugAndPlayDevices) { + $diff_text += "-RedirectPnP = $($cap.DeviceRedirection.PlugAndPlayDevices)`n+RedirectPnP = $redirect_pnp`n" + Set-CAPPropertyValue -Name $name -Property "DeviceRedirection\PlugAndPlayDevices" -Value ([int]$redirect_pnp) -ResultObj $result -WhatIf:$check_mode + $result.changed = $true + } + + if ($null -ne $user_groups) { + $groups_to_remove = @($cap.UserGroups | Where-Object { $user_groups -notcontains $_ }) + $groups_to_add = @($user_groups | Where-Object { $cap.UserGroups -notcontains $_ }) + + $user_groups_diff = $null + foreach ($group in $groups_to_add) { + if (-not $check_mode) { + $return = $wmi_cap | Invoke-CimMethod -MethodName AddUserGroupNames -Arguments @{ UserGroupNames = $group } + if ($return.ReturnValue -ne 0) { + Fail-Json -obj $result -message "Failed to add user group $($group) (code: $($return.ReturnValue))" + } + } + $user_groups_diff += " +$group`n" + $result.changed = $true + } + + foreach ($group in $groups_to_remove) { + if (-not $check_mode) { + $return = $wmi_cap | Invoke-CimMethod -MethodName RemoveUserGroupNames -Arguments @{ UserGroupNames = $group } + if ($return.ReturnValue -ne 0) { + Fail-Json -obj $result -message "Failed to remove user group $($group) (code: $($return.ReturnValue))" + } + } + $user_groups_diff += " -$group`n" + $result.changed = $true + } + + if ($user_groups_diff) { + $diff_text += "~UserGroups`n$user_groups_diff" + } + } + + if ($null -ne $computer_groups) { + $groups_to_remove = @($cap.ComputerGroups | Where-Object { $computer_groups -notcontains $_ }) + $groups_to_add = @($computer_groups | Where-Object { $cap.ComputerGroups -notcontains $_ }) + + $computer_groups_diff = $null + foreach ($group in $groups_to_add) { + if (-not $check_mode) { + $return = $wmi_cap | Invoke-CimMethod -MethodName AddComputerGroupNames -Arguments @{ ComputerGroupNames = $group } + if ($return.ReturnValue -ne 0) { + Fail-Json -obj $result -message "Failed to add computer group $($group) (code: $($return.ReturnValue))" + } + } + $computer_groups_diff += " +$group`n" + $result.changed = $true + } + + foreach ($group in $groups_to_remove) { + if (-not $check_mode) { + $return = $wmi_cap | Invoke-CimMethod -MethodName RemoveComputerGroupNames -Arguments @{ ComputerGroupNames = $group } + if ($return.ReturnValue -ne 0) { + Fail-Json -obj $result -message "Failed to remove computer group $($group) (code: $($return.ReturnValue))" + } + } + $computer_groups_diff += " -$group`n" + $result.changed = $true + } + + if ($computer_groups_diff) { + $diff_text += "~ComputerGroups`n$computer_groups_diff" + } + } + } +} + +if ($diff_mode -and $result.changed -eq $true) { + $result.diff = @{ + prepared = $diff_text + } +} + +Exit-Json $result diff --git a/ansible_collections/community/windows/plugins/modules/win_rds_cap.py b/ansible_collections/community/windows/plugins/modules/win_rds_cap.py new file mode 100644 index 000000000..2513b0474 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_rds_cap.py @@ -0,0 +1,128 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Kevin Subileau (@ksubileau) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_rds_cap +short_description: Manage Connection Authorization Policies (CAP) on a Remote Desktop Gateway server +description: + - Creates, removes and configures a Remote Desktop connection authorization policy (RD CAP). + - A RD CAP allows you to specify the users who can connect to a Remote Desktop Gateway server. +author: + - Kevin Subileau (@ksubileau) +options: + name: + description: + - Name of the connection authorization policy. + type: str + required: yes + state: + description: + - The state of connection authorization policy. + - If C(absent) will ensure the policy is removed. + - If C(present) will ensure the policy is configured and exists. + - If C(enabled) will ensure the policy is configured, exists and enabled. + - If C(disabled) will ensure the policy is configured, exists, but disabled. + type: str + choices: [ absent, enabled, disabled, present ] + default: present + auth_method: + description: + - Specifies how the RD Gateway server authenticates users. + - When a new CAP is created, the default value is C(password). + type: str + choices: [ both, none, password, smartcard ] + order: + description: + - Evaluation order of the policy. + - The CAP in which I(order) is set to a value of '1' is evaluated first. + - By default, a newly created CAP will take the first position. + - If the given value exceed the total number of existing policies, + the policy will take the last position but the evaluation order + will be capped to this number. + type: int + session_timeout: + description: + - The maximum time, in minutes, that a session can be idle. + - A value of zero disables session timeout. + type: int + session_timeout_action: + description: + - The action the server takes when a session times out. + - 'C(disconnect): disconnect the session.' + - 'C(reauth): silently reauthenticate and reauthorize the session.' + type: str + choices: [ disconnect, reauth ] + default: disconnect + idle_timeout: + description: + - Specifies the time interval, in minutes, after which an idle session is disconnected. + - A value of zero disables idle timeout. + type: int + allow_only_sdrts_servers: + description: + - Specifies whether connections are allowed only to Remote Desktop Session Host servers that + enforce Remote Desktop Gateway redirection policy. + type: bool + user_groups: + description: + - A list of user groups that is allowed to connect to the Remote Gateway server. + - Required when a new CAP is created. + type: list + elements: str + computer_groups: + description: + - A list of computer groups that is allowed to connect to the Remote Gateway server. + type: list + elements: str + redirect_clipboard: + description: + - Allow clipboard redirection. + type: bool + redirect_drives: + description: + - Allow disk drive redirection. + type: bool + redirect_printers: + description: + - Allow printers redirection. + type: bool + redirect_serial: + description: + - Allow serial port redirection. + type: bool + redirect_pnp: + description: + - Allow Plug and Play devices redirection. + type: bool +requirements: + - Windows Server 2008R2 (6.1) or higher. + - The Windows Feature "RDS-Gateway" must be enabled. +seealso: +- module: community.windows.win_rds_cap +- module: community.windows.win_rds_rap +- module: community.windows.win_rds_settings +''' + +EXAMPLES = r''' +- name: Create a new RDS CAP with a 30 minutes timeout and clipboard redirection enabled + community.windows.win_rds_cap: + name: My CAP + user_groups: + - BUILTIN\users + session_timeout: 30 + session_timeout_action: disconnect + allow_only_sdrts_servers: yes + redirect_clipboard: yes + redirect_drives: no + redirect_printers: no + redirect_serial: no + redirect_pnp: no + state: enabled +''' + +RETURN = r''' +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_rds_rap.ps1 b/ansible_collections/community/windows/plugins/modules/win_rds_rap.ps1 new file mode 100644 index 000000000..956a812dd --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_rds_rap.ps1 @@ -0,0 +1,318 @@ +#!powershell + +# Copyright: (c) 2018, Kevin Subileau (@ksubileau) +# 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" + +# List of authentication methods as string. Used for parameter validation and conversion to integer flag, so order is important! +$computer_group_types = @("rdg_group", "ad_network_resource_group", "allow_any") +$computer_group_types_wmi = @{rdg_group = "RG"; ad_network_resource_group = "CG"; allow_any = "ALL" } + +$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 + +$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true +$description = Get-AnsibleParam -obj $params -name "description" -type "str" +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent", "present", "enabled", "disabled" +$computer_group_type = Get-AnsibleParam -obj $params -name "computer_group_type" -type "str" -validateset $computer_group_types +$computer_group_failure = ($computer_group_type -eq "ad_network_resource_group" -or $computer_group_type -eq "rdg_group") +$computer_group = Get-AnsibleParam -obj $params -name "computer_group" -type "str" -failifempty $computer_group_failure +$user_groups = Get-AnsibleParam -obj $params -name "user_groups" -type "list" +$allowed_ports = Get-AnsibleParam -obj $params -name "allowed_ports" -type "list" + + +Function ConvertTo-Sid { + [OutputType([string])] + [CmdletBinding()] + param ( + [Parameter(Mandatory, ValueFromPipeline)] + [string[]] + $InputObject + ) + + process { + foreach ($user in $InputObject) { + # RDS uses the UPN format with the builtin domain but Convert-ToSID tries to look this up as a domain. + # Ensure the input value is in the Netlogon format to ensure BUILTIN is resolved properly + if ($user.EndsWith("@builtin", [System.StringComparison]::OrdinalIgnoreCase)) { + $user = "BUILTIN\$($user.Substring(0, $user.Length - 8))" + } + + Convert-ToSID -account_name $user + } + } +} + + +function Get-RAP([string] $name) { + $rap_path = "RDS:\GatewayServer\RAP\$name" + $rap = @{ + Name = $name + } + + # Fetch RAP properties + Get-ChildItem -LiteralPath $rap_path | ForEach-Object { $rap.Add($_.Name, $_.CurrentValue) } + # Convert boolean values + $rap.Enabled = $rap.Status -eq 1 + $rap.Remove("Status") + + # Convert computer group name from UPN to Down-Level Logon format + if ($rap.ComputerGroupType -ne 2) { + $rap.ComputerGroup = Convert-FromSID -sid (ConvertTo-SID -InputObject $rap.ComputerGroup) + } + + # Convert multiple choices values + $rap.ComputerGroupType = $computer_group_types[$rap.ComputerGroupType] + + # Convert allowed ports from string to list + if ($rap.PortNumbers -eq '*') { + $rap.PortNumbers = @("any") + } + else { + $rap.PortNumbers = @($rap.PortNumbers -split ',') + } + + # Fetch RAP user groups in Down-Level Logon format + $rap.UserGroups = @( + Get-ChildItem -LiteralPath "$rap_path\UserGroups" | + Select-Object -ExpandProperty Name | + ForEach-Object { Convert-FromSID -sid (ConvertTo-Sid -InputObject $_) } + ) + + return $rap +} + +function Set-RAPPropertyValue { + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [Parameter(Mandatory = $true)] + [string] $name, + [Parameter(Mandatory = $true)] + [string] $property, + [Parameter(Mandatory = $true)] + $value, + [Parameter()] + $resultobj = @{} + ) + + $rap_path = "RDS:\GatewayServer\RAP\$name" + + try { + Set-Item -LiteralPath "$rap_path\$property" -Value $value -ErrorAction stop + } + catch { + Fail-Json -obj $resultobj -message "Failed to set property $property of RAP ${name}: $($_.Exception.Message)" + } +} + +$result = @{ + changed = $false +} +$diff_text = $null + +# Validate RAP name +if ($name -match "[*/\\;:?`"<>|\t]+") { + Fail-Json -obj $result -message "Invalid character in RAP name." +} + +# Validate user groups +if ($null -ne $user_groups) { + if ($user_groups.Count -lt 1) { + Fail-Json -obj $result -message "Parameter 'user_groups' cannot be an empty list." + } + + $user_groups = $user_groups | ForEach-Object { + $group = $_ + # Test that the group is resolvable on the local machine + $sid = ConvertTo-Sid -InputObject $group + if (!$sid) { + Fail-Json -obj $result -message "$group is not a valid user group on the host machine or domain." + } + + # Return the normalized group name in Down-Level Logon format + Convert-FromSID -sid $sid + } + $user_groups = @($user_groups) +} + +# Validate computer group parameter +if ($computer_group_type -eq "allow_any" -and $null -ne $computer_group) { + Add-Warning -obj $result -message "Parameter 'computer_group' ignored because the computer_group_type is set to allow_any." +} +elseif ($computer_group_type -eq "rdg_group" -and -not (Test-Path -LiteralPath "RDS:\GatewayServer\GatewayManagedComputerGroups\$computer_group")) { + Fail-Json -obj $result -message "$computer_group is not a valid gateway managed computer group" +} +elseif ($computer_group_type -eq "ad_network_resource_group") { + $sid = ConvertTo-Sid -InputObject $computer_group + if (!$sid) { + Fail-Json -obj $result -message "$computer_group is not a valid computer group on the host machine or domain." + } + # Ensure the group name is in Down-Level Logon format + $computer_group = Convert-FromSID -sid $sid +} + +# Validate port numbers +if ($null -ne $allowed_ports) { + foreach ($port in $allowed_ports) { + if (-not ($port -eq "any" -or ($port -is [int] -and $port -ge 1 -and $port -le 65535))) { + Fail-Json -obj $result -message "$port is not a valid port number." + } + } +} + +# Ensure RemoteDesktopServices module is loaded +if ($null -eq (Get-Module -Name RemoteDesktopServices -ErrorAction SilentlyContinue)) { + Import-Module -Name RemoteDesktopServices +} + +# Check if a RAP with the given name already exists +$rap_exist = Test-Path -LiteralPath "RDS:\GatewayServer\RAP\$name" + +if ($state -eq 'absent') { + if ($rap_exist) { + Remove-Item -LiteralPath "RDS:\GatewayServer\RAP\$name" -Recurse -WhatIf:$check_mode + $diff_text += "-[$name]" + $result.changed = $true + } +} +else { + $diff_text_added_prefix = '' + if (-not $rap_exist) { + if ($null -eq $user_groups) { + Fail-Json -obj $result -message "User groups must be defined to create a new RAP." + } + + # Computer group type is required when creating a new RAP. Set it to allow connect to any resource by default. + if ($null -eq $computer_group_type) { + $computer_group_type = "allow_any" + } + + # Create a new RAP + if (-not $check_mode) { + $RapArgs = @{ + Name = $name + ResourceGroupType = 'ALL' + UserGroupNames = $user_groups -join ';' + ProtocolNames = 'RDP' + PortNumbers = '*' + } + $cimParams = @{ + Namespace = "Root\CIMV2\TerminalServices" + ClassName = "Win32_TSGatewayResourceAuthorizationPolicy" + MethodName = "Create" + Arguments = $RapArgs + } + $return = Invoke-CimMethod @cimParams + if ($return.ReturnValue -ne 0) { + Fail-Json -obj $result -message "Failed to create RAP $name (code: $($return.ReturnValue))" + } + } + $rap_exist = -not $check_mode + + $diff_text_added_prefix = '+' + $result.changed = $true + } + + $diff_text += "$diff_text_added_prefix[$name]`n" + + # We cannot configure a RAP that was created above in check mode as it won't actually exist + if ($rap_exist) { + $rap = Get-RAP -Name $name + $wmi_rap = Get-CimInstance -ClassName Win32_TSGatewayResourceAuthorizationPolicy -Namespace Root\CIMv2\TerminalServices -Filter "name='$($name)'" + + if ($state -in @('disabled', 'enabled')) { + $rap_enabled = $state -ne 'disabled' + if ($rap.Enabled -ne $rap_enabled) { + $diff_text += "-State = $(@('disabled', 'enabled')[[int]$rap.Enabled])`n+State = $state`n" + Set-RAPPropertyValue -Name $name -Property Status -Value ([int]$rap_enabled) -ResultObj $result -WhatIf:$check_mode + $result.changed = $true + } + } + + if ($null -ne $description -and $description -ne $rap.Description) { + Set-RAPPropertyValue -Name $name -Property Description -Value $description -ResultObj $result -WhatIf:$check_mode + $diff_text += "-Description = $($rap.Description)`n+Description = $description`n" + $result.changed = $true + } + + if ($null -ne $allowed_ports -and @(Compare-Object $rap.PortNumbers $allowed_ports -SyncWindow 0).Count -ne 0) { + $diff_text += "-AllowedPorts = [$($rap.PortNumbers -join ',')]`n+AllowedPorts = [$($allowed_ports -join ',')]`n" + if ($allowed_ports -contains 'any') { $allowed_ports = '*' } + Set-RAPPropertyValue -Name $name -Property PortNumbers -Value $allowed_ports -ResultObj $result -WhatIf:$check_mode + $result.changed = $true + } + + if ($null -ne $computer_group_type -and $computer_group_type -ne $rap.ComputerGroupType) { + $diff_text += "-ComputerGroupType = $($rap.ComputerGroupType)`n+ComputerGroupType = $computer_group_type`n" + if ($computer_group_type -ne "allow_any") { + $diff_text += "+ComputerGroup = $computer_group`n" + } + $return = $wmi_rap | Invoke-CimMethod -MethodName SetResourceGroup -Arguments @{ + ResourceGroupName = $computer_group + ResourceGroupType = $computer_group_types_wmi.$($computer_group_type) + } + if ($return.ReturnValue -ne 0) { + Fail-Json -obj $result -message "Failed to set computer group type to $($computer_group_type) (code: $($return.ReturnValue))" + } + + $result.changed = $true + + } + elseif ($null -ne $computer_group -and $computer_group -ne $rap.ComputerGroup) { + $diff_text += "-ComputerGroup = $($rap.ComputerGroup)`n+ComputerGroup = $computer_group`n" + $return = $wmi_rap | Invoke-CimMethod -MethodName SetResourceGroup -Arguments @{ + ResourceGroupName = $computer_group + ResourceGroupType = $computer_group_types_wmi.$($rap.ComputerGroupType) + } + if ($return.ReturnValue -ne 0) { + Fail-Json -obj $result -message "Failed to set computer group name to $($computer_group) (code: $($return.ReturnValue))" + } + $result.changed = $true + } + + if ($null -ne $user_groups) { + $groups_to_remove = @($rap.UserGroups | Where-Object { $user_groups -notcontains $_ }) + $groups_to_add = @($user_groups | Where-Object { $rap.UserGroups -notcontains $_ }) + + $user_groups_diff = $null + foreach ($group in $groups_to_add) { + if (-not $check_mode) { + $return = $wmi_rap | Invoke-CimMethod -MethodName AddUserGroupNames -Arguments @{ UserGroupNames = $group } + if ($return.ReturnValue -ne 0) { + Fail-Json -obj $result -message "Failed to add user group $($group) (code: $($return.ReturnValue))" + } + } + $user_groups_diff += " +$group`n" + $result.changed = $true + } + + foreach ($group in $groups_to_remove) { + if (-not $check_mode) { + $return = $wmi_rap | Invoke-CimMethod -MethodName RemoveUserGroupNames -Arguments @{ UserGroupNames = $group } + if ($return.ReturnValue -ne 0) { + Fail-Json -obj $result -message "Failed to remove user group $($group) (code: $($return.ReturnValue))" + } + } + $user_groups_diff += " -$group`n" + $result.changed = $true + } + + if ($user_groups_diff) { + $diff_text += "~UserGroups`n$user_groups_diff" + } + } + } +} + +if ($diff_mode -and $result.changed -eq $true) { + $result.diff = @{ + prepared = $diff_text + } +} + +Exit-Json $result diff --git a/ansible_collections/community/windows/plugins/modules/win_rds_rap.py b/ansible_collections/community/windows/plugins/modules/win_rds_rap.py new file mode 100644 index 000000000..24ca0f771 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_rds_rap.py @@ -0,0 +1,86 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Kevin Subileau (@ksubileau) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_rds_rap +short_description: Manage Resource Authorization Policies (RAP) on a Remote Desktop Gateway server +description: + - Creates, removes and configures a Remote Desktop resource authorization policy (RD RAP). + - A RD RAP allows you to specify the network resources (computers) that users can connect + to remotely through a Remote Desktop Gateway server. +author: + - Kevin Subileau (@ksubileau) +options: + name: + description: + - Name of the resource authorization policy. + required: yes + state: + description: + - The state of resource authorization policy. + - If C(absent) will ensure the policy is removed. + - If C(present) will ensure the policy is configured and exists. + - If C(enabled) will ensure the policy is configured, exists and enabled. + - If C(disabled) will ensure the policy is configured, exists, but disabled. + type: str + choices: [ absent, disabled, enabled, present ] + default: present + description: + description: + - Optional description of the resource authorization policy. + type: str + user_groups: + description: + - List of user groups that are associated with this resource authorization policy (RAP). + A user must belong to one of these groups to access the RD Gateway server. + - Required when a new RAP is created. + type: list + elements: str + allowed_ports: + description: + - List of port numbers through which connections are allowed for this policy. + - To allow connections through any port, specify 'any'. + type: list + elements: str + computer_group_type: + description: + - 'The computer group type:' + - 'C(rdg_group): RD Gateway-managed group' + - 'C(ad_network_resource_group): Active Directory Domain Services network resource group' + - 'C(allow_any): Allow users to connect to any network resource.' + type: str + choices: [ rdg_group, ad_network_resource_group, allow_any ] + computer_group: + description: + - The computer group name that is associated with this resource authorization policy (RAP). + - This is required when I(computer_group_type) is C(rdg_group) or C(ad_network_resource_group). + type: str +requirements: + - Windows Server 2008R2 (6.1) or higher. + - The Windows Feature "RDS-Gateway" must be enabled. +seealso: +- module: community.windows.win_rds_cap +- module: community.windows.win_rds_rap +- module: community.windows.win_rds_settings +''' + +EXAMPLES = r''' +- name: Create a new RDS RAP + community.windows.win_rds_rap: + name: My RAP + description: Allow all users to connect to any resource through ports 3389 and 3390 + user_groups: + - BUILTIN\users + computer_group_type: allow_any + allowed_ports: + - 3389 + - 3390 + state: enabled +''' + +RETURN = r''' +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_rds_settings.ps1 b/ansible_collections/community/windows/plugins/modules/win_rds_settings.ps1 new file mode 100644 index 000000000..7927f3335 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_rds_settings.ps1 @@ -0,0 +1,95 @@ +#!powershell + +# Copyright: (c) 2018, Kevin Subileau (@ksubileau) +# 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" + +# List of ssl bridging methods as string. Used for parameter validation and conversion to integer flag, so order is important! +$ssl_bridging_methods = @("none", "https_http", "https_https") + +$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 + +$certificate = Get-AnsibleParam $params -name "certificate_hash" -type "str" +$max_connections = Get-AnsibleParam $params -name "max_connections" -type "int" +$ssl_bridging = Get-AnsibleParam -obj $params -name "ssl_bridging" -type "str" -validateset $ssl_bridging_methods +$enable_only_messaging_capable_clients = Get-AnsibleParam $params -name "enable_only_messaging_capable_clients" -type "bool" + +$result = @{ + changed = $false +} +$diff_text = $null + +# Ensure RemoteDesktopServices module is loaded +if ($null -eq (Get-Module -Name RemoteDesktopServices -ErrorAction SilentlyContinue)) { + Import-Module -Name RemoteDesktopServices +} + +if ($null -ne $certificate) { + # Validate cert path + $cert_path = "cert:\LocalMachine\My\$certificate" + If (-not (Test-Path -LiteralPath $cert_path) ) { + Fail-Json -obj $result -message "Unable to locate certificate at $cert_path" + } + + # Get current certificate hash + $current_cert = (Get-Item -LiteralPath "RDS:\GatewayServer\SSLCertificate\Thumbprint").CurrentValue + if ($current_cert -ne $certificate) { + Set-Item -LiteralPath "RDS:\GatewayServer\SSLCertificate\Thumbprint" -Value $certificate -WhatIf:$check_mode + $diff_text += "-Certificate = $current_cert`n+Certificate = $certificate`n" + $result.changed = $true + } +} + +if ($null -ne $max_connections) { + # Set the correct value for unlimited connections + # TODO Use a more explicit value, maybe a string (ex: "max", "none" or "unlimited") ? + If ($max_connections -eq -1) { + $max_connections = (Get-Item -LiteralPath "RDS:\GatewayServer\MaxConnectionsAllowed").CurrentValue + } + + # Get current connections limit + $current_max_connections = (Get-Item -LiteralPath "RDS:\GatewayServer\MaxConnections").CurrentValue + if ($current_max_connections -ne $max_connections) { + Set-Item -LiteralPath "RDS:\GatewayServer\MaxConnections" -Value $max_connections -WhatIf:$check_mode + $diff_text += "-MaxConnections = $current_max_connections`n+MaxConnections = $max_connections`n" + $result.changed = $true + } +} + +if ($null -ne $ssl_bridging) { + $current_ssl_bridging = (Get-Item -LiteralPath "RDS:\GatewayServer\SSLBridging").CurrentValue + # Convert the integer value to its representative string + $current_ssl_bridging_str = $ssl_bridging_methods[$current_ssl_bridging] + + if ($current_ssl_bridging_str -ne $ssl_bridging) { + Set-Item -LiteralPath "RDS:\GatewayServer\SSLBridging" -Value ([array]::IndexOf($ssl_bridging_methods, $ssl_bridging)) -WhatIf:$check_mode + $diff_text += "-SSLBridging = $current_ssl_bridging_str`n+SSLBridging = $ssl_bridging`n" + $result.changed = $true + } +} + +if ($null -ne $enable_only_messaging_capable_clients) { + $current_enable_only_messaging_capable_clients = (Get-Item -LiteralPath "RDS:\GatewayServer\EnableOnlyMessagingCapableClients").CurrentValue + # Convert the integer value to boolean + $current_enable_only_messaging_capable_clients = $current_enable_only_messaging_capable_clients -eq 1 + + if ($current_enable_only_messaging_capable_clients -ne $enable_only_messaging_capable_clients) { + Set-Item -LiteralPath "RDS:\GatewayServer\EnableOnlyMessagingCapableClients" -Value ([int]$enable_only_messaging_capable_clients) -WhatIf:$check_mode + $diff_text += "-EnableOnlyMessagingCapableClients = $current_enable_only_messaging_capable_clients`n" + $diff_text += "+EnableOnlyMessagingCapableClients = $enable_only_messaging_capable_clients`n" + $result.changed = $true + } +} + +if ($diff_mode -and $result.changed -eq $true) { + $result.diff = @{ + prepared = $diff_text + } +} + +Exit-Json $result diff --git a/ansible_collections/community/windows/plugins/modules/win_rds_settings.py b/ansible_collections/community/windows/plugins/modules/win_rds_settings.py new file mode 100644 index 000000000..e5370b054 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_rds_settings.py @@ -0,0 +1,57 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Kevin Subileau (@ksubileau) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_rds_settings +short_description: Manage main settings of a Remote Desktop Gateway server +description: + - Configure general settings of a Remote Desktop Gateway server. +author: + - Kevin Subileau (@ksubileau) +options: + certificate_hash: + description: + - Certificate hash (thumbprint) for the Remote Desktop Gateway server. The certificate hash is the unique identifier for the certificate. + type: str + max_connections: + description: + - The maximum number of connections allowed. + - If set to C(0), no new connections are allowed. + - If set to C(-1), the number of connections is unlimited. + type: int + ssl_bridging: + description: + - Specifies whether to use SSL Bridging. + - 'C(none): no SSL bridging.' + - 'C(https_http): HTTPS-HTTP bridging.' + - 'C(https_https): HTTPS-HTTPS bridging.' + type: str + choices: [ https_http, https_https, none ] + enable_only_messaging_capable_clients: + description: + - If enabled, only clients that support logon messages and administrator messages can connect. + type: bool +requirements: + - Windows Server 2008R2 (6.1) or higher. + - The Windows Feature "RDS-Gateway" must be enabled. +seealso: +- module: community.windows.win_rds_cap +- module: community.windows.win_rds_rap +- module: community.windows.win_rds_settings +''' + +EXAMPLES = r''' +- name: Configure the Remote Desktop Gateway + community.windows.win_rds_settings: + certificate_hash: B0D0FA8408FC67B230338FCA584D03792DA73F4C + max_connections: 50 + notify: + - Restart TSGateway service +''' + +RETURN = r''' +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_region.ps1 b/ansible_collections/community/windows/plugins/modules/win_region.ps1 new file mode 100644 index 000000000..90713c12e --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_region.ps1 @@ -0,0 +1,444 @@ +#!powershell + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -PowerShell Ansible.ModuleUtils.AddType +#AnsibleRequires -CSharpUtil Ansible.Basic + +$spec = @{ + options = @{ + location = @{ type = "str" } + format = @{ type = "str" } + unicode_language = @{ type = "str" } + copy_settings = @{ type = "bool"; default = $false } + } + supports_check_mode = $true +} +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$check_mode = $module.CheckMode + +$location = $module.Params.location +$format = $module.Params.format +$unicode_language = $module.Params.unicode_language +$copy_settings = $module.Params.copy_settings + +$module.Result.restart_required = $false + +# This is used to get the format values based on the LCType enum based through. When running Vista/7/2008/200R2 +Add-CSharpType -AnsibleModule $module -References @' +using System; +using System.Text; +using System.Runtime.InteropServices; +using System.ComponentModel; + +namespace Ansible.WinRegion { + + public class NativeMethods + { + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern int GetLocaleInfoEx( + String lpLocaleName, + UInt32 LCType, + StringBuilder lpLCData, + int cchData); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern int GetSystemDefaultLocaleName( + IntPtr lpLocaleName, + int cchLocaleName); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern int GetUserDefaultLocaleName( + IntPtr lpLocaleName, + int cchLocaleName); + + [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 = 0x80000003; // HKU + 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(); } + } + + public class LocaleHelper { + private String Locale; + + public LocaleHelper(String locale) { + Locale = locale; + } + + public String GetValueFromType(UInt32 LCType) { + StringBuilder data = new StringBuilder(500); + int result = NativeMethods.GetLocaleInfoEx(Locale, LCType, data, 500); + if (result == 0) + throw new Win32Exception("Error getting locale info with legacy method"); + + return data.ToString(); + } + } +} +'@ + + +Function Get-LastWin32ExceptionMessage { + param([int]$ErrorCode) + $exp = New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList $ErrorCode + $exp_msg = "{0} (Win32 ErrorCode {1} - 0x{1:X8})" -f $exp.Message, $ErrorCode + return $exp_msg +} + +Function Get-SystemLocaleName { + $max_length = 85 # LOCALE_NAME_MAX_LENGTH + $ptr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($max_length) + + try { + $res = [Ansible.WinRegion.NativeMethods]::GetSystemDefaultLocaleName($ptr, $max_length) + + if ($res -eq 0) { + $err_code = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() + $msg = Get-LastWin32ExceptionMessage -Error $err_code + $module.FailJson("Failed to get system locale: $msg") + } + + $system_locale = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($ptr) + } + finally { + [System.Runtime.InteropServices.Marshal]::FreeHGlobal($ptr) + } + + return $system_locale +} + +Function Get-UserLocaleName { + $max_length = 85 # LOCALE_NAME_MAX_LENGTH + $ptr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($max_length) + + try { + $res = [Ansible.WinRegion.NativeMethods]::GetUserDefaultLocaleName($ptr, $max_length) + + if ($res -eq 0) { + $err_code = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() + $msg = Get-LastWin32ExceptionMessage -Error $err_code + $module.FailJson("Failed to get user locale: $msg") + } + + $user_locale = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($ptr) + } + finally { + [System.Runtime.InteropServices.Marshal]::FreeHGlobal($ptr) + } + + return $user_locale +} + +Function Get-ValidGeoId($cultures) { + $geo_ids = @() + foreach ($culture in $cultures) { + try { + $geo_id = [System.Globalization.RegionInfo]$culture.Name + $geo_ids += $geo_id.GeoId + } + catch {} + } + $geo_ids +} + +Function Test-RegistryProperty($reg_key, $property) { + $type = Get-ItemProperty -LiteralPath $reg_key -Name $property -ErrorAction SilentlyContinue + if ($null -eq $type) { + $false + } + else { + $true + } +} + +Function Copy-RegistryKey($source, $target) { + # Using Copy-Item -Recurse is giving me weird results, doing it recursively + Copy-Item -LiteralPath $source -Destination $target -WhatIf:$check_mode + + foreach ($key in Get-ChildItem -LiteralPath $source) { + $sourceKey = "$source\$($key.PSChildName)" + $targetKey = (Get-Item -LiteralPath $source).PSChildName + Copy-RegistryKey -source "$sourceKey" -target "$target\$targetKey" + } +} + +Function Set-UserLocale($culture) { + $reg_key = 'HKCU:\Control Panel\International' + + $lookup = New-Object Ansible.WinRegion.LocaleHelper($culture) + # hex values are from http://www.pinvoke.net/default.aspx/kernel32/GetLocaleInfoEx.html + $wanted_values = @{ + Locale = '{0:x8}' -f ([System.Globalization.CultureInfo]$culture).LCID + LocaleName = $culture + s1159 = $lookup.GetValueFromType(0x00000028) + s2359 = $lookup.GetValueFromType(0x00000029) + sCountry = $lookup.GetValueFromType(0x00000006) + sCurrency = $lookup.GetValueFromType(0x00000014) + sDate = $lookup.GetValueFromType(0x0000001D) + sDecimal = $lookup.GetValueFromType(0x0000000E) + sGrouping = $lookup.GetValueFromType(0x00000010) + sLanguage = $lookup.GetValueFromType(0x00000003) # LOCALE_ABBREVLANGNAME + sList = $lookup.GetValueFromType(0x0000000C) + sLongDate = $lookup.GetValueFromType(0x00000020) + sMonDecimalSep = $lookup.GetValueFromType(0x00000016) + sMonGrouping = $lookup.GetValueFromType(0x00000018) + sMonThousandSep = $lookup.GetValueFromType(0x00000017) + sNativeDigits = $lookup.GetValueFromType(0x00000013) + sNegativeSign = $lookup.GetValueFromType(0x00000051) + sPositiveSign = $lookup.GetValueFromType(0x00000050) + sShortDate = $lookup.GetValueFromType(0x0000001F) + sThousand = $lookup.GetValueFromType(0x0000000F) + sTime = $lookup.GetValueFromType(0x0000001E) + sTimeFormat = $lookup.GetValueFromType(0x00001003) + sYearMonth = $lookup.GetValueFromType(0x00001006) + iCalendarType = $lookup.GetValueFromType(0x00001009) + iCountry = $lookup.GetValueFromType(0x00000005) + iCurrDigits = $lookup.GetValueFromType(0x00000019) + iCurrency = $lookup.GetValueFromType(0x0000001B) + iDate = $lookup.GetValueFromType(0x00000021) + iDigits = $lookup.GetValueFromType(0x00000011) + NumShape = $lookup.GetValueFromType(0x00001014) # LOCALE_IDIGITSUBSTITUTION + iFirstDayOfWeek = $lookup.GetValueFromType(0x0000100C) + iFirstWeekOfYear = $lookup.GetValueFromType(0x0000100D) + iLZero = $lookup.GetValueFromType(0x00000012) + iMeasure = $lookup.GetValueFromType(0x0000000D) + iNegCurr = $lookup.GetValueFromType(0x0000001C) + iNegNumber = $lookup.GetValueFromType(0x00001010) + iPaperSize = $lookup.GetValueFromType(0x0000100A) + iTime = $lookup.GetValueFromType(0x00000023) + iTimePrefix = $lookup.GetValueFromType(0x00001005) + iTLZero = $lookup.GetValueFromType(0x00000025) + } + + if (Test-RegistryProperty -reg_key $reg_key -property 'sShortTime') { + # sShortTime was added after Vista, will check anyway and add in the value if it exists + $wanted_values.sShortTime = $lookup.GetValueFromType(0x00000079) + } + + $properties = Get-ItemProperty -LiteralPath $reg_key + foreach ($property in $properties.PSObject.Properties) { + if (Test-RegistryProperty -reg_key $reg_key -property $property.Name) { + $name = $property.Name + $old_value = $property.Value + $new_value = $wanted_values.$name + + if ($new_value -ne $old_value) { + Set-ItemProperty -LiteralPath $reg_key -Name $name -Value $new_value -WhatIf:$check_mode + $module.Result.changed = $true + } + } + } +} + +Function Set-SystemLocaleLegacy($unicode_language) { + # For when Get/Set-WinSystemLocale is not available (Pre Windows 8 and Server 2012) + $current_language_value = (Get-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Nls\Language').Default + $wanted_language_value = '{0:x4}' -f ([System.Globalization.CultureInfo]$unicode_language).LCID + if ($current_language_value -ne $wanted_language_value) { + Set-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Nls\Language' -Name 'Default' -Value $wanted_language_value -WhatIf:$check_mode + $module.Result.changed = $true + $module.Result.restart_required = $true + } + + # This reads from the non registry (Default) key, the extra prop called (Default) see below for more details + $current_locale_value = (Get-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Nls\Locale')."(Default)" + $wanted_locale_value = '{0:x8}' -f ([System.Globalization.CultureInfo]$unicode_language).LCID + if ($current_locale_value -ne $wanted_locale_value) { + # Need to use .net to write property value, Locale has 2 (Default) properties + # 1: The actual (Default) property, we don't want to change Set-ItemProperty writes to this value when using (Default) + # 2: A property called (Default), this is what we want to change and only .net SetValue can do this one + if (-not $check_mode) { + $hive = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey("LocalMachine", $env:COMPUTERNAME) + $key = $hive.OpenSubKey("SYSTEM\CurrentControlSet\Control\Nls\Locale", $true) + $key.SetValue("(Default)", $wanted_locale_value, [Microsoft.Win32.RegistryValueKind]::String) + } + $module.Result.changed = $true + $module.Result.restart_required = $true + } + + $codepage_path = 'HKLM:\SYSTEM\CurrentControlSet\Control\Nls\CodePage' + $current_codepage_info = Get-ItemProperty -LiteralPath $codepage_path + $wanted_codepage_info = ([System.Globalization.CultureInfo]::GetCultureInfo($unicode_language)).TextInfo + + $current_a_cp = $current_codepage_info.ACP + $current_oem_cp = $current_codepage_info.OEMCP + $current_mac_cp = $current_codepage_info.MACCP + $wanted_a_cp = $wanted_codepage_info.ANSICodePage + $wanted_oem_cp = $wanted_codepage_info.OEMCodePage + $wanted_mac_cp = $wanted_codepage_info.MacCodePage + + if ($current_a_cp -ne $wanted_a_cp) { + Set-ItemProperty -LiteralPath $codepage_path -Name 'ACP' -Value $wanted_a_cp -WhatIf:$check_mode + $module.Result.changed = $true + $module.Result.restart_required = $true + } + if ($current_oem_cp -ne $wanted_oem_cp) { + Set-ItemProperty -LiteralPath $codepage_path -Name 'OEMCP' -Value $wanted_oem_cp -WhatIf:$check_mode + $module.Result.changed = $true + $module.Result.restart_required = $true + } + if ($current_mac_cp -ne $wanted_mac_cp) { + Set-ItemProperty -LiteralPath $codepage_path -Name 'MACCP' -Value $wanted_mac_cp -WhatIf:$check_mode + $module.Result.changed = $true + $module.Result.restart_required = $true + } +} + +if ($null -eq $format -and $null -eq $location -and $null -eq $unicode_language) { + $module.FailJson("An argument for 'format', 'location' or 'unicode_language' needs to be supplied") +} +else { + $valid_cultures = [System.Globalization.CultureInfo]::GetCultures('AllCultures') + $valid_geoids = Get-ValidGeoId -cultures $valid_cultures + + if ($null -ne $location) { + if ($valid_geoids -notcontains $location) { + $module.FailJson("The argument location '$location' does not contain a valid Geo ID") + } + } + + if ($null -ne $format) { + if ($valid_cultures.Name -notcontains $format) { + $module.FailJson("The argument format '$format' does not contain a valid Culture Name") + } + } + + if ($null -ne $unicode_language) { + if ($valid_cultures.Name -notcontains $unicode_language) { + $module.FailJson("The argument unicode_language '$unicode_language' does not contain a valid Culture Name") + } + } +} + +if ($null -ne $location) { + # Get-WinHomeLocation was only added in Server 2012 and above + # Use legacy option if older + if (Get-Command 'Get-WinHomeLocation' -ErrorAction SilentlyContinue) { + $current_location = (Get-WinHomeLocation).GeoId + if ($current_location -ne $location) { + if (-not $check_mode) { + Set-WinHomeLocation -GeoId $location + } + $module.Result.changed = $true + } + } + else { + $current_location = (Get-ItemProperty -LiteralPath 'HKCU:\Control Panel\International\Geo').Nation + if ($current_location -ne $location) { + Set-ItemProperty -LiteralPath 'HKCU:\Control Panel\International\Geo' -Name 'Nation' -Value $location -WhatIf:$check_mode + $module.Result.changed = $true + } + } +} + +if ($null -ne $format) { + # Cannot use Get/Set-Culture as that fails to get and set the culture when running in the PSRP runspace. + $current_format = Get-UserLocaleName + if ($current_format -ne $format) { + Set-UserLocale -culture $format + $module.Result.changed = $true + } +} + +if ($null -ne $unicode_language) { + # Get/Set-WinSystemLocale was only added in Server 2012 and above, use legacy option if older + if (Get-Command 'Get-WinSystemLocale' -ErrorAction SilentlyContinue) { + $current_unicode_language = Get-SystemLocaleName + if ($current_unicode_language -ne $unicode_language) { + if (-not $check_mode) { + Set-WinSystemLocale -SystemLocale $unicode_language + } + $module.Result.changed = $true + $module.Result.restart_required = $true + } + } + else { + Set-SystemLocaleLegacy -unicode_language $unicode_language + } +} + +if ($copy_settings -eq $true -and $module.Result.changed -eq $true) { + if (-not $check_mode) { + $null = New-PSDrive -Name HKU -PSProvider Registry -Root Registry::HKEY_USERS + + if (Test-Path -LiteralPath HKU:\ANSIBLE) { + $module.Warn("hive already loaded at HKU:\ANSIBLE, had to unload hive for win_region to continue") + [Ansible.WinRegion.Hive]::UnloadHive("ANSIBLE") + } + + $loaded_hive = New-Object -TypeName Ansible.WinRegion.Hive -ArgumentList "ANSIBLE", 'C:\Users\Default\NTUSER.DAT' + try { + $sids = 'ANSIBLE', '.DEFAULT', 'S-1-5-19', 'S-1-5-20' + foreach ($sid in $sids) { + Copy-RegistryKey -source "HKCU:\Keyboard Layout" -target "HKU:\$sid" + Copy-RegistryKey -source "HKCU:\Control Panel\International" -target "HKU:\$sid\Control Panel" + Copy-RegistryKey -source "HKCU:\Control Panel\Input Method" -target "HKU:\$sid\Control Panel" + } + } + finally { + $loaded_hive.Dispose() + } + + Remove-PSDrive HKU + } + $module.Result.changed = $true +} + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_region.py b/ansible_collections/community/windows/plugins/modules/win_region.py new file mode 100644 index 000000000..1c6200d80 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_region.py @@ -0,0 +1,95 @@ +#!/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) + +DOCUMENTATION = r''' +module: win_region +short_description: Set the region and format settings +description: + - Set the location settings of a Windows Server. + - Set the format settings of a Windows Server. + - Set the unicode language settings of a Windows Server. + - Copy across these settings to the default profile. +options: + location: + description: + - The location to set for the current user, see + U(https://msdn.microsoft.com/en-us/library/dd374073.aspx) + for a list of GeoIDs you can use and what location it relates to. + - This needs to be set if C(format) or C(unicode_language) is not + set. + type: str + format: + description: + - The language format to set for the current user, see + U(https://msdn.microsoft.com/en-us/library/system.globalization.cultureinfo.aspx) + for a list of culture names to use. + - This needs to be set if C(location) or C(unicode_language) is not set. + type: str + unicode_language: + description: + - The unicode language format to set for all users, see + U(https://msdn.microsoft.com/en-us/library/system.globalization.cultureinfo.aspx) + for a list of culture names to use. + - This needs to be set if C(location) or C(format) is not set. After setting this + value a reboot is required for it to take effect. + type: str + copy_settings: + description: + - This will copy the current format and location values to new user + profiles and the welcome screen. This will only run if + C(location), C(format) or C(unicode_language) has resulted in a + change. If this process runs then it will always result in a + change. + type: bool + default: no +seealso: +- module: community.windows.win_timezone +author: +- Jordan Borean (@jborean93) +''' + +EXAMPLES = r''' +- name: Set the region format to English United States + community.windows.win_region: + format: en-US + +- name: Set the region format to English Australia and copy settings to new profiles + community.windows.win_region: + format: en-AU + copy_settings: yes + +- name: Set the location to United States + community.windows.win_region: + location: 244 + +# Reboot when region settings change +- name: Set the unicode language to English Great Britain, reboot if required + community.windows.win_region: + unicode_language: en-GB + register: result + +- ansible.windows.win_reboot: + when: result.restart_required + +# Reboot when format, location or unicode has changed +- name: Set format, location and unicode to English Australia and copy settings, reboot if required + community.windows.win_region: + location: 12 + format: en-AU + unicode_language: en-AU + register: result + +- ansible.windows.win_reboot: + when: result.restart_required +''' + +RETURN = r''' +restart_required: + description: Whether a reboot is required for the change to take effect. + returned: success + type: bool + sample: true +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_regmerge.ps1 b/ansible_collections/community/windows/plugins/modules/win_regmerge.ps1 new file mode 100644 index 000000000..8d511cc20 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_regmerge.ps1 @@ -0,0 +1,99 @@ +#!powershell + +# Copyright: (c) 2015, Jon Hawkesworth (@jhawkesworth) <jhawkesworth@protonmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#Requires -Module Ansible.ModuleUtils.ArgvParser +#Requires -Module Ansible.ModuleUtils.CommandUtil +#Requires -Module Ansible.ModuleUtils.Legacy + +Function Convert-RegistryPath { + Param ( + [parameter(Mandatory = $True)] + [ValidateNotNullOrEmpty()]$Path + ) + + $output = $Path -replace "HKLM:", "HKLM" + $output = $output -replace "HKCU:", "HKCU" + + Return $output +} + +$result = @{ + changed = $false +} +$params = Parse-Args $args + +$path = Get-AnsibleParam -obj $params -name "path" -type "path" -failifempty $true -resultobj $result +$compare_to = Get-AnsibleParam -obj $params -name "compare_to" -type "str" -resultobj $result + +# check it looks like a reg key, warn if key not present - will happen first time +# only accepting PS-Drive style key names (starting with HKLM etc, not HKEY_LOCAL_MACHINE etc) + +$do_comparison = $False + +If ($compare_to) { + $compare_to_key = $params.compare_to.ToString() + If (Test-Path $compare_to_key -pathType container ) { + $do_comparison = $True + } + Else { + $result.compare_to_key_found = $false + } +} + +If ( $do_comparison -eq $True ) { + $guid = [guid]::NewGuid() + $exported_path = $env:TEMP + "\" + $guid.ToString() + 'ansible_win_regmerge.reg' + + $expanded_compare_key = Convert-RegistryPath ($compare_to_key) + + # export from the reg key location to a file + $reg_args = Argv-ToString -Arguments @("reg.exe", "EXPORT", $expanded_compare_key, $exported_path) + $res = Run-Command -command $reg_args + if ($res.rc -ne 0) { + $result.rc = $res.rc + $result.stdout = $res.stdout + $result.stderr = $res.stderr + Fail-Json -obj $result -message "error exporting registry '$expanded_compare_key' to '$exported_path'" + } + + # compare the two files + $comparison_result = Compare-Object -ReferenceObject $(Get-Content $path) -DifferenceObject $(Get-Content $exported_path) + + If ($null -ne $comparison_result -and (Get-Member -InputObject $comparison_result -Name "count" -MemberType Properties )) { + # Something is different, actually do reg merge + $reg_import_args = Argv-ToString -Arguments @("reg.exe", "IMPORT", $path) + $res = Run-Command -command $reg_import_args + if ($res.rc -ne 0) { + $result.rc = $res.rc + $result.stdout = $res.stdout + $result.stderr = $res.stderr + Fail-Json -obj $result -message "error importing registry values from '$path'" + } + $result.changed = $true + $result.difference_count = $comparison_result.count + } + Else { + $result.difference_count = 0 + } + + Remove-Item $exported_path + $result.compared = $true + +} +Else { + # not comparing, merge and report changed + $reg_import_args = Argv-ToString -Arguments @("reg.exe", "IMPORT", $path) + $res = Run-Command -command $reg_import_args + if ($res.rc -ne 0) { + $result.rc = $res.rc + $result.stdout = $res.stdout + $result.stderr = $res.stderr + Fail-Json -obj $result -message "error importing registry value from '$path'" + } + $result.changed = $true + $result.compared = $false +} + +Exit-Json $result diff --git a/ansible_collections/community/windows/plugins/modules/win_regmerge.py b/ansible_collections/community/windows/plugins/modules/win_regmerge.py new file mode 100644 index 000000000..073f54c9a --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_regmerge.py @@ -0,0 +1,74 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2015, Jon Hawkesworth (@jhawkesworth) <jhawkesworth@protonmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_regmerge +short_description: Merges the contents of a registry file into the Windows registry +description: + - Wraps the reg.exe command to import the contents of a registry file. + - Suitable for use with registry files created using M(ansible.windows.win_template). + - Windows registry files have a specific format and must be constructed correctly with carriage return and line feed line endings otherwise they will not + be merged. + - Exported registry files often start with a Byte Order Mark which must be removed if the file is to templated using M(ansible.windows.win_template). + - Registry file format is described at U(https://support.microsoft.com/en-us/kb/310516) + - See also M(ansible.windows.win_template), M(ansible.windows.win_regedit) +options: + path: + description: + - The full path including file name to the registry file on the remote machine to be merged + type: path + required: yes + compare_key: + description: + - The parent key to use when comparing the contents of the registry to the contents of the file. Needs to be in HKLM or HKCU part of registry. + Use a PS-Drive style path for example HKLM:\SOFTWARE not HKEY_LOCAL_MACHINE\SOFTWARE + If not supplied, or the registry key is not found, no comparison will be made, and the module will report changed. + type: str +notes: + - Organise your registry files so that they contain a single root registry + key if you want to use the compare_to functionality. + - This module does not force registry settings to be in the state + described in the file. If registry settings have been modified externally + the module will merge the contents of the file but continue to report + differences on subsequent runs. + - To force registry change, use M(ansible.windows.win_regedit) with C(state=absent) before + using C(community.windows.win_regmerge). +seealso: +- module: ansible.windows.win_reg_stat +- module: ansible.windows.win_regedit +author: +- Jon Hawkesworth (@jhawkesworth) +''' + +EXAMPLES = r''' +- name: Merge in a registry file without comparing to current registry + community.windows.win_regmerge: + path: C:\autodeploy\myCompany-settings.reg + +- name: Compare and merge registry file + community.windows.win_regmerge: + path: C:\autodeploy\myCompany-settings.reg + compare_to: HKLM:\SOFTWARE\myCompany +''' + +RETURN = r''' +compare_to_key_found: + description: whether the parent registry key has been found for comparison + returned: when comparison key not found in registry + type: bool + sample: false +difference_count: + description: number of differences between the registry and the file + returned: changed + type: int + sample: 1 +compared: + description: whether a comparison has taken place between the registry and the file + returned: when a comparison key has been supplied and comparison has been attempted + type: bool + sample: true +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_robocopy.ps1 b/ansible_collections/community/windows/plugins/modules/win_robocopy.ps1 new file mode 100644 index 000000000..2d3e05805 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_robocopy.ps1 @@ -0,0 +1,148 @@ +#!powershell + +# Copyright: (c) 2015, Corwin Brown <corwin.brown@maxpoint.com> +# 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 +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false + +$src = Get-AnsibleParam -obj $params -name "src" -type "path" -failifempty $true +$dest = Get-AnsibleParam -obj $params -name "dest" -type "path" -failifempty $true +$purge = Get-AnsibleParam -obj $params -name "purge" -type "bool" -default $false +$recurse = Get-AnsibleParam -obj $params -name "recurse" -type "bool" -default $false +$flags = Get-AnsibleParam -obj $params -name "flags" -type "str" + +$result = @{ + changed = $false + dest = $dest + purge = $purge + recurse = $recurse + src = $src +} + +# Search for an Error Message +# Robocopy seems to display an error after 3 '-----' separator lines +Function SearchForError($cmd_output, $default_msg) { + $separator_count = 0 + $error_msg = $default_msg + ForEach ($line in $cmd_output) { + if (-not $line) { + continue + } + + if ($separator_count -ne 3) { + if (Select-String -InputObject $line -pattern "^(\s+)?(\-+)(\s+)?$") { + $separator_count += 1 + } + } + else { + if (Select-String -InputObject $line -pattern "error") { + $error_msg = $line + break + } + } + } + + return $error_msg +} + +if (-not (Test-Path -Path $src)) { + Fail-Json $result "$src does not exist!" +} + +# Build Arguments +$robocopy_opts = @($src, $dest) + +if ($check_mode) { + $robocopy_opts += "/l" +} + +if ($null -eq $flags) { + if ($purge) { + $robocopy_opts += "/purge" + } + + if ($recurse) { + $robocopy_opts += "/e" + } +} +else { + ForEach ($f in $flags.split(" ")) { + $robocopy_opts += $f + } +} + +$result.flags = $flags +$result.cmd = "$robocopy $robocopy_opts" + +Try { + $robocopy_output = &robocopy $robocopy_opts + $rc = $LASTEXITCODE +} +Catch { + Fail-Json $result "Error synchronizing $src to $dest! Msg: $($_.Exception.Message)" +} + +$result.msg = "Success" +$result.output = $robocopy_output +$result.return_code = $rc # Backward compatibility +$result.rc = $rc + +switch ($rc) { + + 0 { + $result.msg = "No files copied." + } + 1 { + $result.msg = "Files copied successfully!" + $result.changed = $true + $result.failed = $false + } + 2 { + $result.msg = "Some Extra files or directories were detected. No files were copied." + Add-Warning $result $result.msg + $result.failed = $false + } + 3 { + $result.msg = "(2+1) Some files were copied. Additional files were present." + Add-Warning $result $result.msg + $result.changed = $true + $result.failed = $false + } + 4 { + $result.msg = "Some mismatched files or directories were detected. Housekeeping might be required!" + Add-Warning $result $result.msg + $result.changed = $true + $result.failed = $false + } + 5 { + $result.msg = "(4+1) Some files were copied. Some files were mismatched." + Add-Warning $result $result.msg + $result.changed = $true + $result.failed = $false + } + 6 { + $result.msg = "(4+2) Additional files and mismatched files exist. No files were copied." + $result.failed = $false + } + 7 { + $result.msg = "(4+1+2) Files were copied, a file mismatch was present, and additional files were present." + Add-Warning $result $result.msg + $result.changed = $true + $result.failed = $false + } + 8 { + Fail-Json $result (SearchForError $robocopy_output "Some files or directories could not be copied!") + } + { @(9, 10, 11, 12, 13, 14, 15) -contains $_ } { + Fail-Json $result (SearchForError $robocopy_output "Fatal error. Check log message!") + } + 16 { + Fail-Json $result (SearchForError $robocopy_output "Serious Error! No files were copied! Do you have permissions to access $src and $dest?") + } + +} + +Exit-Json $result diff --git a/ansible_collections/community/windows/plugins/modules/win_robocopy.py b/ansible_collections/community/windows/plugins/modules/win_robocopy.py new file mode 100644 index 000000000..21a918750 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_robocopy.py @@ -0,0 +1,139 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2015, Corwin Brown <blakfeld@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_robocopy +short_description: Synchronizes the contents of two directories using Robocopy +description: +- Synchronizes the contents of files/directories from a source to destination. +- Under the hood this just calls out to RoboCopy, since that should be available + on most modern Windows systems. +options: + src: + description: + - Source file/directory to sync. + type: path + required: yes + dest: + description: + - Destination file/directory to sync (Will receive contents of src). + type: path + required: yes + recurse: + description: + - Includes all subdirectories (Toggles the C(/e) flag to RoboCopy). + - If C(flags) is set, this will be ignored. + type: bool + default: no + purge: + description: + - Deletes any files/directories found in the destination that do not exist in the source. + - Toggles the C(/purge) flag to RoboCopy. + - If C(flags) is set, this will be ignored. + type: bool + default: no + flags: + description: + - Directly supply Robocopy flags. + - If set, C(purge) and C(recurse) will be ignored. + type: str +notes: +- This is not a complete port of the M(ansible.posix.synchronize) module. Unlike the M(ansible.posix.synchronize) + module this only performs the sync/copy on the remote machine, not from the Ansible controller to the remote machine. +- This module does not currently support all Robocopy flags. +seealso: +- module: ansible.posix.synchronize +- module: ansible.windows.win_copy +author: +- Corwin Brown (@blakfeld) +''' + +EXAMPLES = r''' +- name: Sync the contents of one directory to another + community.windows.win_robocopy: + src: C:\DirectoryOne + dest: C:\DirectoryTwo + +- name: Sync the contents of one directory to another, including subdirectories + community.windows.win_robocopy: + src: C:\DirectoryOne + dest: C:\DirectoryTwo + recurse: yes + +- name: Sync the contents of one directory to another, and remove any files/directories found in destination that do not exist in the source + community.windows.win_robocopy: + src: C:\DirectoryOne + dest: C:\DirectoryTwo + purge: yes + +- name: Sync content in recursive mode, removing any files/directories found in destination that do not exist in the source + community.windows.win_robocopy: + src: C:\DirectoryOne + dest: C:\DirectoryTwo + recurse: yes + purge: yes + +- name: Sync two directories in recursive and purging mode, specifying additional special flags + community.windows.win_robocopy: + src: C:\DirectoryOne + dest: C:\DirectoryTwo + flags: /E /PURGE /XD SOME_DIR /XF SOME_FILE /MT:32 + +- name: Sync one file from a remote UNC path in recursive and purging mode, specifying additional special flags + community.windows.win_robocopy: + src: \\Server1\Directory One + dest: C:\DirectoryTwo + flags: file.zip /E /PURGE /XD SOME_DIR /XF SOME_FILE /MT:32 +''' + +RETURN = r''' +cmd: + description: The used command line. + returned: always + type: str + sample: robocopy C:\DirectoryOne C:\DirectoryTwo /e /purge +src: + description: The Source file/directory of the sync. + returned: always + type: str + sample: C:\Some\Path +dest: + description: The Destination file/directory of the sync. + returned: always + type: str + sample: C:\Some\Path +recurse: + description: Whether or not the recurse flag was toggled. + returned: always + type: bool + sample: false +purge: + description: Whether or not the purge flag was toggled. + returned: always + type: bool + sample: false +flags: + description: Any flags passed in by the user. + returned: always + type: str + sample: /e /purge +rc: + description: The return code returned by robocopy. + returned: success + type: int + sample: 1 +output: + description: The output of running the robocopy command. + returned: success + type: str + sample: "------------------------------------\\n ROBOCOPY :: Robust File Copy for Windows \\n------------------------------------\\n " +msg: + description: Output interpreted into a concise message. + returned: always + type: str + sample: No files copied! +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_route.ps1 b/ansible_collections/community/windows/plugins/modules/win_route.ps1 new file mode 100644 index 000000000..58129fa46 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_route.ps1 @@ -0,0 +1,112 @@ +#!powershell + +# Copyright: (c) 2016, Daniele Lazzari <lazzari@mailup.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#Requires -Module Ansible.ModuleUtils.Legacy + +# win_route (Add or remove a network static route) + +$params = Parse-Args $args -supports_check_mode $true + +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -default $false +$dest = Get-AnsibleParam -obj $params -name "destination" -type "str" -failifempty $true +$gateway = Get-AnsibleParam -obj $params -name "gateway" -type "str" +$metric = Get-AnsibleParam -obj $params -name "metric" -type "int" -default 1 +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateSet "present", "absent" +$result = @{ + "changed" = $false + "output" = "" +} + +Function Add-Route { + Param ( + [Parameter(Mandatory = $true)] + [string]$Destination, + [Parameter(Mandatory = $true)] + [string]$Gateway, + [Parameter(Mandatory = $true)] + [int]$Metric, + [Parameter(Mandatory = $true)] + [bool]$CheckMode + ) + + + $IpAddress = $Destination.split('/')[0] + + # Check if the static route is already present + $Route = Get-CimInstance win32_ip4PersistedrouteTable -Filter "Destination = '$($IpAddress)'" + if (!($Route)) { + try { + # Find Interface Index + $InterfaceIndex = Find-NetRoute -RemoteIPAddress $Gateway | Select-Object -First 1 -ExpandProperty InterfaceIndex + + # Add network route + $routeParams = @{ + DestinationPrefix = $Destination + NextHop = $Gateway + InterfaceIndex = $InterfaceIndex + RouteMetric = $Metric + ErrorAction = "Stop" + WhatIf = $CheckMode + } + New-NetRoute @routeParams | Out-Null + $result.changed = $true + $result.output = "Route added" + + } + catch { + $ErrorMessage = $_.Exception.Message + Fail-Json $result $ErrorMessage + } + } + else { + $result.output = "Static route already exists" + } + +} + +Function Remove-Route { + Param ( + [Parameter(Mandatory = $true)] + [string]$Destination, + [bool]$CheckMode + ) + $IpAddress = $Destination.split('/')[0] + $Route = Get-CimInstance win32_ip4PersistedrouteTable -Filter "Destination = '$($IpAddress)'" + if ($Route) { + try { + + Remove-NetRoute -DestinationPrefix $Destination -Confirm:$false -ErrorAction Stop -WhatIf:$CheckMode + $result.changed = $true + $result.output = "Route removed" + } + catch { + $ErrorMessage = $_.Exception.Message + Fail-Json $result $ErrorMessage + } + } + else { + $result.output = "No route to remove" + } + +} + +# Set gateway if null +if (!($gateway)) { + $gateway = "0.0.0.0" +} + + +if ($state -eq "present") { + + Add-Route -Destination $dest -Gateway $gateway -Metric $metric -CheckMode $check_mode + +} +else { + + Remove-Route -Destination $dest -CheckMode $check_mode + +} + +Exit-Json $result diff --git a/ansible_collections/community/windows/plugins/modules/win_route.py b/ansible_collections/community/windows/plugins/modules/win_route.py new file mode 100644 index 000000000..655ec11eb --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_route.py @@ -0,0 +1,62 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2017, Daniele Lazzari <lazzari@mailup.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_route +short_description: Add or remove a static route +description: + - Add or remove a static route. +options: + destination: + description: + - Destination IP address in CIDR format (ip address/prefix length). + type: str + required: yes + gateway: + description: + - The gateway used by the static route. + - If C(gateway) is not provided it will be set to C(0.0.0.0). + type: str + metric: + description: + - Metric used by the static route. + type: int + default: 1 + state: + description: + - If C(absent), it removes a network static route. + - If C(present), it adds a network static route. + type: str + choices: [ absent, present ] + default: present +notes: + - Works only with Windows 2012 R2 and newer. +author: +- Daniele Lazzari (@dlazz) +''' + +EXAMPLES = r''' +--- +- name: Add a network static route + community.windows.win_route: + destination: 192.168.2.10/32 + gateway: 192.168.1.1 + metric: 1 + state: present + +- name: Remove a network static route + community.windows.win_route: + destination: 192.168.2.10/32 + state: absent +''' +RETURN = r''' +output: + description: A message describing the task result. + returned: always + type: str + sample: "Route added" +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_say.ps1 b/ansible_collections/community/windows/plugins/modules/win_say.ps1 new file mode 100644 index 000000000..4f66430eb --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_say.ps1 @@ -0,0 +1,108 @@ +#!powershell + +# Copyright: (c) 2016, Jon Hawkesworth (@jhawkesworth) <jhawkesworth@protonmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic + +$spec = @{ + options = @{ + msg = @{ type = "str" } + msg_file = @{ type = "path" } + start_sound_path = @{ type = "path" } + end_sound_path = @{ type = "path" } + voice = @{ type = "str" } + speech_speed = @{ type = "int"; default = 0 } + } + mutually_exclusive = @( + , @('msg', 'msg_file') + ) + required_one_of = @( + , @('msg', 'msg_file', 'start_sound_path', 'end_sound_path') + ) + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + + +$msg = $module.Params.msg +$msg_file = $module.Params.msg_file +$start_sound_path = $module.Params.start_sound_path +$end_sound_path = $module.Params.end_sound_path +$voice = $module.Params.voice +$speech_speed = $module.Params.speech_speed + +if ($speech_speed -lt -10 -or $speech_speed -gt 10) { + $module.FailJson("speech_speed needs to be an integer in the range -10 to 10. The value $speech_speed is outside this range.") +} + +$words = $null + +if ($msg_file) { + if (-not (Test-Path -LiteralPath $msg_file)) { + $msg = -join @( + "Message file $msg_file could not be found or opened. " + "Ensure you have specified the full path to the file, and the ansible windows user has permission to read the file." + ) + $module.FailJson($msg) + } + $words = Get-Content -LiteralPath $msg_file | Out-String +} + +if ($msg) { + $words = $msg +} + +if ($start_sound_path) { + if (-not (Test-Path -LiteralPath $start_sound_path)) { + $msg = -join @( + "Start sound file $start_sound_path could not be found or opened. " + "Ensure you have specified the full path to the file, and the ansible windows user has permission to read the file." + ) + $module.FailJson($msg) + } + if (-not $module.CheckMode) { + (new-object Media.SoundPlayer $start_sound_path).playSync() + } +} + +if ($words) { + Add-Type -AssemblyName System.speech + $tts = New-Object System.Speech.Synthesis.SpeechSynthesizer + if ($voice) { + try { + $tts.SelectVoice($voice) + } + catch [System.Management.Automation.MethodInvocationException] { + $module.Result.voice_info = "Could not load voice '$voice', using system default voice." + $module.Warn("Could not load voice '$voice', using system default voice.") + } + } + + $module.Result.voice = $tts.Voice.Name + if ($speech_speed -ne 0) { + $tts.Rate = $speech_speed + } + if (-not $module.CheckMode) { + $tts.Speak($words) + } + $tts.Dispose() +} + +if ($end_sound_path) { + if (-not (Test-Path -LiteralPath $end_sound_path)) { + $msg = -join @( + "End sound file $start_sound_path could not be found or opened. " + "Ensure you have specified the full path to the file, and the ansible windows user has permission to read the file." + ) + $module.FailJson($msg) + } + if (-not $module.CheckMode) { + (new-object Media.SoundPlayer $end_sound_path).playSync() + } +} + +$module.Result.message_text = $words.ToString() + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_say.py b/ansible_collections/community/windows/plugins/modules/win_say.py new file mode 100644 index 000000000..6ee107b7e --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_say.py @@ -0,0 +1,108 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2016, Jon Hawkesworth (@jhawkesworth) <jhawkesworth@protonmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_say +short_description: Text to speech module for Windows to speak messages and optionally play sounds +description: + - Uses .NET libraries to convert text to speech and optionally play .wav sounds. Audio Service needs to be running and some kind of speakers or + headphones need to be attached to the windows target(s) for the speech to be audible. +options: + msg: + description: + - The text to be spoken. + - Use either C(msg) or C(msg_file). + - Optional so that you can use this module just to play sounds. + type: str + msg_file: + description: + - Full path to a windows format text file containing the text to be spoken. + - Use either C(msg) or C(msg_file). + - Optional so that you can use this module just to play sounds. + type: path + voice: + description: + - Which voice to use. See notes for how to discover installed voices. + - If the requested voice is not available the default voice will be used. + Example voice names from Windows 10 are C(Microsoft Zira Desktop) and C(Microsoft Hazel Desktop). + type: str + speech_speed: + description: + - How fast or slow to speak the text. + - Must be an integer value in the range -10 to 10. + - -10 is slowest, 10 is fastest. + type: int + default: 0 + start_sound_path: + description: + - Full path to a C(.wav) file containing a sound to play before the text is spoken. + - Useful on conference calls to alert other speakers that ansible has something to say. + type: path + end_sound_path: + description: + - Full path to a C(.wav) file containing a sound to play after the text has been spoken. + - Useful on conference calls to alert other speakers that ansible has finished speaking. + type: path +notes: + - Needs speakers or headphones to do anything useful. + - | + To find which voices are installed, run the following Powershell commands. + + Add-Type -AssemblyName System.Speech + $speech = New-Object -TypeName System.Speech.Synthesis.SpeechSynthesizer + $speech.GetInstalledVoices() | ForEach-Object { $_.VoiceInfo } + $speech.Dispose() + + - Speech can be surprisingly slow, so it's best to keep message text short. +seealso: +- module: community.windows.win_msg +- module: community.windows.win_toast +author: +- Jon Hawkesworth (@jhawkesworth) +''' + +EXAMPLES = r''' +- name: Warn of impending deployment + community.windows.win_say: + msg: Warning, deployment commencing in 5 minutes, please log out. + +- name: Using a different voice and a start sound + community.windows.win_say: + start_sound_path: C:\Windows\Media\ding.wav + msg: Warning, deployment commencing in 5 minutes, please log out. + voice: Microsoft Hazel Desktop + +- name: With start and end sound + community.windows.win_say: + start_sound_path: C:\Windows\Media\Windows Balloon.wav + msg: New software installed + end_sound_path: C:\Windows\Media\chimes.wav + +- name: Text from file example + community.windows.win_say: + start_sound_path: C:\Windows\Media\Windows Balloon.wav + msg_file: AppData\Local\Temp\morning_report.txt + end_sound_path: C:\Windows\Media\chimes.wav +''' + +RETURN = r''' +message_text: + description: The text that the module attempted to speak. + returned: success + type: str + sample: "Warning, deployment commencing in 5 minutes." +voice: + description: The voice used to speak the text. + returned: success + type: str + sample: Microsoft Hazel Desktop +voice_info: + description: The voice used to speak the text. + returned: when requested voice could not be loaded + type: str + sample: Could not load voice TestVoice, using system default voice +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_scheduled_task.ps1 b/ansible_collections/community/windows/plugins/modules/win_scheduled_task.ps1 new file mode 100644 index 000000000..02cd012f8 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_scheduled_task.ps1 @@ -0,0 +1,1234 @@ +#!powershell + +# Copyright: (c) 2015, Peter Mounce <public@neverrunwithscissors.com> +# Copyright: (c) 2015, Michael Perzel <michaelperzel@gmail.com> +# Copyright: (c) 2017, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -PowerShell Ansible.ModuleUtils.AddType +#Requires -Module Ansible.ModuleUtils.Legacy +#Requires -Module Ansible.ModuleUtils.SID + +$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 +$_remote_tmp = Get-AnsibleParam $params "_ansible_remote_tmp" -type "path" -default $env:TMP + +$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true +$path = Get-AnsibleParam -obj $params -name "path" -type "str" -default "\" +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent", "present" + +# task actions, list of dicts [{path, arguments, working_directory}] +$actions = Get-AnsibleParam -obj $params -name "actions" -type "list" + +# task triggers, list of dicts [{ type, ... }] +$triggers = Get-AnsibleParam -obj $params -name "triggers" -type "list" + +# task Principal properties +$display_name = Get-AnsibleParam -obj $params -name "display_name" -type "str" +$group = Get-AnsibleParam -obj $params -name "group" -type "str" +$logon_options = "none", "password", "s4u", "interactive_token", "group", "service_account", "interactive_token_or_password" +$logon_type = Get-AnsibleParam -obj $params -name "logon_type" -type "str" -validateset $logon_options +$run_level = Get-AnsibleParam -obj $params -name "run_level" -type "str" -validateset "limited", "highest" -aliases "runlevel" +$username = Get-AnsibleParam -obj $params -name "username" -type "str" -aliases "user" +$password = Get-AnsibleParam -obj $params -name "password" -type "str" +$update_password = Get-AnsibleParam -obj $params -name "update_password" -type "bool" -default $true + +# task RegistrationInfo properties +$author = Get-AnsibleParam -obj $params -name "author" -type "str" +$date = Get-AnsibleParam -obj $params -name "date" -type "str" +$description = Get-AnsibleParam -obj $params -name "description" -type "str" +$source = Get-AnsibleParam -obj $params -name "source" -type "str" +$version = Get-AnsibleParam -obj $params -name "version" -type "str" + +# task Settings properties +$allow_demand_start = Get-AnsibleParam -obj $params -name "allow_demand_start" -type "bool" +$allow_hard_terminate = Get-AnsibleParam -obj $params -name "allow_hard_terminate" -type "bool" +# https://msdn.microsoft.com/en-us/library/windows/desktop/aa383486(v=vs.85).aspx +$compatibility = Get-AnsibleParam -obj $params -name "compatibility" -type "int" +$delete_expired_task_after = Get-AnsibleParam -obj $params -name "delete_expired_task_after" -type "str" # time string PT... +$disallow_start_if_on_batteries = Get-AnsibleParam -obj $params -name "disallow_start_if_on_batteries" -type "bool" +$enabled = Get-AnsibleParam -obj $params -name "enabled" -type "bool" +$execution_time_limit = Get-AnsibleParam -obj $params -name "execution_time_limit" -type "str" # PT72H +$hidden = Get-AnsibleParam -obj $params -name "hidden" -type "bool" +# TODO: support for $idle_settings, needs to be created as a COM object +# https://msdn.microsoft.com/en-us/library/windows/desktop/aa383507(v=vs.85).aspx +$multiple_instances = Get-AnsibleParam -obj $params -name "multiple_instances" -type "int" +# TODO: support for $network_settings, needs to be created as a COM object +# https://msdn.microsoft.com/en-us/library/windows/desktop/aa383512(v=vs.85).aspx +$priority = Get-AnsibleParam -obj $params -name "priority" -type "int" +$restart_count = Get-AnsibleParam -obj $params -name "restart_count" -type "int" +$restart_interval = Get-AnsibleParam -obj $params -name "restart_interval" -type "str" # time string PT.. +$run_only_if_idle = Get-AnsibleParam -obj $params -name "run_only_if_idle" -type "bool" +$run_only_if_network_available = Get-AnsibleParam -obj $params -name "run_only_if_network_available" -type "bool" +$start_when_available = Get-AnsibleParam -obj $params -name "start_when_available" -type "bool" +$stop_if_going_on_batteries = Get-AnsibleParam -obj $params -name "stop_if_going_on_batteries" -type "bool" +$wake_to_run = Get-AnsibleParam -obj $params -name "wake_to_run" -type "bool" + +$result = @{ + changed = $false +} + +if ($diff_mode) { + $result.diff = @{} +} + +Add-CSharpType -TempPath $_remote_tmp -References @' +public enum TASK_ACTION_TYPE // https://msdn.microsoft.com/en-us/library/windows/desktop/aa383553(v=vs.85).aspx +{ + TASK_ACTION_EXEC = 0, + // The below are not supported and are only kept for documentation purposes + TASK_ACTION_COM_HANDLER = 5, + TASK_ACTION_SEND_EMAIL = 6, + TASK_ACTION_SHOW_MESSAGE = 7 +} + +public enum TASK_CREATION // https://msdn.microsoft.com/en-us/library/windows/desktop/aa382538(v=vs.85).aspx +{ + TASK_VALIDATE_ONLY = 0x1, + TASK_CREATE = 0x2, + TASK_UPDATE = 0x4, + TASK_CREATE_OR_UPDATE = 0x6, + TASK_DISABLE = 0x8, + TASK_DONT_ADD_PRINCIPAL_ACE = 0x10, + TASK_IGNORE_REGISTRATION_TRIGGERS = 0x20 +} + +public enum TASK_LOGON_TYPE // https://msdn.microsoft.com/en-us/library/windows/desktop/aa383566(v=vs.85).aspx +{ + TASK_LOGON_NONE = 0, + TASK_LOGON_PASSWORD = 1, + TASK_LOGON_S4U = 2, + TASK_LOGON_INTERACTIVE_TOKEN = 3, + TASK_LOGON_GROUP = 4, + TASK_LOGON_SERVICE_ACCOUNT = 5, + TASK_LOGON_INTERACTIVE_TOKEN_OR_PASSWORD = 6 +} + +public enum TASK_RUN_LEVEL // https://msdn.microsoft.com/en-us/library/windows/desktop/aa380747(v=vs.85).aspx +{ + TASK_RUNLEVEL_LUA = 0, + TASK_RUNLEVEL_HIGHEST = 1 +} + +public enum TASK_TRIGGER_TYPE2 // https://msdn.microsoft.com/en-us/library/windows/desktop/aa383915(v=vs.85).aspx +{ + TASK_TRIGGER_EVENT = 0, + TASK_TRIGGER_TIME = 1, + TASK_TRIGGER_DAILY = 2, + TASK_TRIGGER_WEEKLY = 3, + TASK_TRIGGER_MONTHLY = 4, + TASK_TRIGGER_MONTHLYDOW = 5, + TASK_TRIGGER_IDLE = 6, + TASK_TRIGGER_REGISTRATION = 7, + TASK_TRIGGER_BOOT = 8, + TASK_TRIGGER_LOGON = 9, + TASK_TRIGGER_SESSION_STATE_CHANGE = 11 +} + +public enum TASK_SESSION_STATE_CHANGE_TYPE // https://docs.microsoft.com/en-us/windows/win32/api/taskschd/ne-taskschd-task_session_state_change_type +{ + TASK_CONSOLE_CONNECT = 1, + TASK_CONSOLE_DISCONNECT = 2, + TASK_REMOTE_CONNECT = 3, + TASK_REMOTE_DISCONNECT = 4, + TASK_SESSION_LOCK = 7, + TASK_SESSION_UNLOCK = 8 +} +'@ + +######################## +### HELPER FUNCTIONS ### +######################## +Function Convert-SnakeToPascalCase($snake) { + # very basic function to convert snake_case to PascalCase for use in COM + # objects + [regex]$regex = "_(\w)" + $pascal_case = $regex.Replace($snake, { $args[0].Value.Substring(1).ToUpper() }) + $capitalised = $pascal_case.Substring(0, 1).ToUpper() + $pascal_case.Substring(1) + + return $capitalised +} + +Function Compare-Property($property_name, $parent_property, $map, $enum_map = $null) { + $changes = [System.Collections.ArrayList]@() + + # loop through the passed in map and compare values + # Name = The name of property in the COM object + # Value = The new value to compare the existing value with + foreach ($entry in $map.GetEnumerator()) { + $new_value = $entry.Value + + if ($null -ne $new_value) { + $property_name = $entry.Name + $existing_value = $parent_property.$property_name + if ($existing_value -cne $new_value) { + try { + $parent_property.$property_name = $new_value + } + catch { + Fail-Json -obj $result -message "failed to set $property_name property '$property_name' to '$new_value': $($_.Exception.Message)" + } + + if ($null -ne $enum_map -and $enum_map.ContainsKey($property_name)) { + $enum = [type]$enum_map.$property_name + $existing_value = [Enum]::ToObject($enum, $existing_value) + $new_value = [Enum]::ToObject($enum, $new_value) + } + [void]$changes.Add("-$property_name=$existing_value`n+$property_name=$new_value") + } + } + } + + return , $changes +} + +Function Set-PropertyForComObject($com_object, $name, $arg, $value) { + $com_name = Convert-SnakeToPascalCase -snake $arg + try { + $com_object.$com_name = $value + } + catch { + Fail-Json -obj $result -message "failed to set $name property '$com_name' to '$value': $($_.Exception.Message)" + } +} + +Function Compare-PropertyList { + Param( + $collection, # the collection COM object to manipulate, this must contains the Create method + [string]$property_name, # human friendly name of the property object, e.g. action/trigger + [Array]$new, # a list of new properties, passed in by Ansible + [Array]$existing, # a list of existing properties from the COM object collection + [Hashtable]$map, # metadata for the collection, see below for the structure + [string]$enum # the parent enum name for type value + ) + <## map metadata structure + { + collection type [TASK_ACTION_TYPE] for Actions or [TASK_TRIGGER_TYPE2] for Triggers { + mandatory = list of mandatory properties for this type, ansible input name not the COM name + optional = list of optional properties that could be set for this type + # maps the ansible input object name to the COM name, e.g. working_directory = WorkingDirectory + map = { + ansible input name = COM name + } + } + }##> + # used by both Actions and Triggers to compare the collections of that property + + $enum = [type]$enum + $changes = [System.Collections.ArrayList]@() + $new_count = $new.Count + $existing_count = $existing.Count + + for ($i = 0; $i -lt $new_count; $i++) { + if ($i -lt $existing_count) { + $existing_property = $existing[$i] + } + else { + $existing_property = $null + } + $new_property = $new[$i] + + # get the type of the property, for action this is set automatically + if (-not $new_property.ContainsKey("type")) { + Fail-Json -obj $result -message "entry for $property_name must contain a type key" + } + $type = $new_property.type + $valid_types = $map.Keys + $property_map = $map.$type + + # now let's validate the args for the property + $mandatory_args = $property_map.mandatory + $optional_args = $property_map.optional + $total_args = $mandatory_args + $optional_args + + # validate the mandatory arguments + foreach ($mandatory_arg in $mandatory_args) { + if (-not $new_property.ContainsKey($mandatory_arg)) { + $msg = "mandatory key '$mandatory_arg' for $($property_name) is not set, mandatory keys are '$($mandatory_args -join "', '")'" + Fail-Json -obj $result -message $msg + } + } + # throw a warning if in invalid key was set + foreach ($entry in $new_property.GetEnumerator()) { + $key = $entry.Name + if ($key -notin $total_args -and $key -ne "type") { + $msg = "key '$key' for $($property_name) entry is not valid and will be ignored, valid keys are '$($total_args -join "', '")'" + Add-Warning -obj $result -message $msg + } + } + + # now we have validated the input and have gotten the metadata, let's + # get the diff string + if ($null -eq $existing_property) { + # we have more properties than before,just add to the new + # properties list + $diff_list = [System.Collections.ArrayList]@() + + foreach ($property_arg in $total_args) { + if ($new_property.ContainsKey($property_arg)) { + $com_name = Convert-SnakeToPascalCase -snake $property_arg + $property_value = $new_property.$property_arg + + if ($property_value -is [Hashtable]) { + foreach ($kv in $property_value.GetEnumerator()) { + $sub_com_name = Convert-SnakeToPascalCase -snake $kv.Key + $sub_property_value = $kv.Value + [void]$diff_list.Add("+$com_name.$sub_com_name=$sub_property_value") + } + } + else { + [void]$diff_list.Add("+$com_name=$property_value") + } + } + } + + [void]$changes.Add("+$property_name[$i] = {`n +Type=$type`n $($diff_list -join ",`n ")`n+}") + } + elseif ([Enum]::ToObject($enum, $existing_property.Type) -ne $type) { + # the types are different so we need to change + $diff_list = [System.Collections.ArrayList]@() + + if ($existing_property.Type -notin $valid_types) { + [void]$diff_list.Add("-UNKNOWN TYPE $($existing_property.Type)") + foreach ($property_args in $total_args) { + if ($new_property.ContainsKey($property_arg)) { + $com_name = Convert-SnakeToPascalCase -snake $property_arg + $property_value = $new_property.$property_arg + + if ($property_value -is [Hashtable]) { + foreach ($kv in $property_value.GetEnumerator()) { + $sub_com_name = Convert-SnakeToPascalCase -snake $kv.Key + $sub_property_value = $kv.Value + [void]$diff_list.Add("+$com_name.$sub_com_name=$sub_property_value") + } + } + else { + [void]$diff_list.Add("+$com_name=$property_value") + } + } + } + } + else { + # we know the types of the existing property + $existing_type = [Enum]::ToObject([TASK_TRIGGER_TYPE2], $existing_property.Type) + [void]$diff_list.Add("-Type=$existing_type") + [void]$diff_list.Add("+Type=$type") + foreach ($property_arg in $total_args) { + $com_name = Convert-SnakeToPascalCase -snake $property_arg + $property_value = $new_property.$property_arg + $existing_value = $existing_property.$com_name + + if ($property_value -is [Hashtable]) { + foreach ($kv in $property_value.GetEnumerator()) { + $sub_property_value = $kv.Value + $sub_com_name = Convert-SnakeToPascalCase -snake $kv.Key + $sub_existing_value = $existing_property.$com_name.$sub_com_name + + if ($null -ne $sub_property_value) { + [void]$diff_list.Add("+$com_name.$sub_com_name=$sub_property_value") + } + + if ($null -ne $sub_existing_value) { + [void]$diff_list.Add("-$com_name.$sub_com_name=$sub_existing_value") + } + } + } + else { + if ($null -ne $property_value) { + [void]$diff_list.Add("+$com_name=$property_value") + } + + if ($null -ne $existing_value) { + [void]$diff_list.Add("-$com_name=$existing_value") + } + } + } + } + + [void]$changes.Add("$property_name[$i] = {`n $($diff_list -join ",`n ")`n}") + } + else { + # compare the properties of existing and new + $diff_list = [System.Collections.ArrayList]@() + + foreach ($property_arg in $total_args) { + $com_name = Convert-SnakeToPascalCase -snake $property_arg + $property_value = $new_property.$property_arg + $existing_value = $existing_property.$com_name + + if ($property_value -is [Hashtable]) { + foreach ($kv in $property_value.GetEnumerator()) { + $sub_property_value = $kv.Value + + if ($null -ne $sub_property_value) { + $sub_com_name = Convert-SnakeToPascalCase -snake $kv.Key + $sub_existing_value = $existing_property.$com_name.$sub_com_name + + if ($sub_property_value -cne $sub_existing_value) { + [void]$diff_list.Add("-$com_name.$sub_com_name=$sub_existing_value") + [void]$diff_list.Add("+$com_name.$sub_com_name=$sub_property_value") + } + } + } + } + elseif ($null -ne $property_value -and $property_value -cne $existing_value) { + [void]$diff_list.Add("-$com_name=$existing_value") + [void]$diff_list.Add("+$com_name=$property_value") + } + } + + if ($diff_list.Count -gt 0) { + [void]$changes.Add("$property_name[$i] = {`n $($diff_list -join ",`n ")`n}") + } + } + + # finally rebuild the new property collection + $new_object = $collection.Create($type) + foreach ($property_arg in $total_args) { + $new_value = $new_property.$property_arg + if ($new_value -is [Hashtable]) { + $com_name = Convert-SnakeToPascalCase -snake $property_arg + $new_object_property = $new_object.$com_name + + foreach ($kv in $new_value.GetEnumerator()) { + $value = $kv.Value + if ($null -ne $value) { + Set-PropertyForComObject -com_object $new_object_property -name $property_name -arg $kv.Key -value $value + } + } + } + elseif ($null -ne $new_value) { + Set-PropertyForComObject -com_object $new_object -name $property_name -arg $property_arg -value $new_value + } + } + } + + # if there were any extra properties not in the new list, create diff str + if ($existing_count -gt $new_count) { + for ($i = $new_count; $i -lt $existing_count; $i++) { + $diff_list = [System.Collections.ArrayList]@() + $existing_property = $existing[$i] + $existing_type = [Enum]::ToObject($enum, $existing_property.Type) + + if ($map.ContainsKey($existing_type)) { + $property_map = $map.$existing_type + $property_args = $property_map.mandatory + $property_map.optional + + foreach ($property_arg in $property_args) { + $com_name = Convert-SnakeToPascalCase -snake $property_arg + $existing_value = $existing_property.$com_name + if ($null -ne $existing_value) { + [void]$diff_list.Add("-$com_name=$existing_value") + } + } + } + else { + [void]$diff_list.Add("-UNKNOWN TYPE $existing_type") + } + + [void]$changes.Add("-$property_name[$i] = {`n $($diff_list -join ",`n ")`n-}") + } + } + + return , $changes +} + +Function Compare-Action($task_definition) { + # compares the Actions property and returns a list of list of changed + # actions for use in a diff string + # ActionCollection - https://msdn.microsoft.com/en-us/library/windows/desktop/aa446804(v=vs.85).aspx + # Action - https://msdn.microsoft.com/en-us/library/windows/desktop/aa446803(v=vs.85).aspx + if ($null -eq $actions) { + return , [System.Collections.ArrayList]@() + } + + $task_actions = $task_definition.Actions + $existing_count = $task_actions.Count + + # because we clear the actions and re-add them to keep the order, we need + # to convert the existing actions to a new list. + # The Item property in actions starts at 1 + $existing_actions = [System.Collections.ArrayList]@() + for ($i = 1; $i -le $existing_count; $i++) { + [void]$existing_actions.Add($task_actions.Item($i)) + } + if ($existing_count -gt 0) { + $task_actions.Clear() + } + + $map = @{ + [TASK_ACTION_TYPE]::TASK_ACTION_EXEC = @{ + mandatory = @('path') + optional = @('arguments', 'working_directory') + } + } + $changes = Compare-PropertyList -collection $task_actions -property_name "action" -new $actions -existing $existing_actions -map $map -enum TASK_ACTION_TYPE + + return , $changes +} + +Function Compare-Principal($task_definition, $task_definition_xml) { + # compares the Principal property and returns a list of changed objects for + # use in a diff string + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa382071(v=vs.85).aspx + $principal_map = @{ + DisplayName = $display_name + LogonType = $logon_type + RunLevel = $run_level + } + $enum_map = @{ + LogonType = "TASK_LOGON_TYPE" + RunLevel = "TASK_RUN_LEVEL" + } + $task_principal = $task_definition.Principal + $changes = Compare-Property -property_name "Principal" -parent_property $task_principal -map $principal_map -enum_map $enum_map + + # Principal.UserId and GroupId only returns the username portion of the + # username, skipping the domain or server name. This makes the + # comparison process useless so we need to parse the task XML to get + # the actual sid/username. Depending on OS version this could be the SID + # or it could be the username, we need to handle that accordingly + $principal_username_sid = $task_definition_xml.Task.Principals.Principal.UserId + if ($null -ne $principal_username_sid -and $principal_username_sid -notmatch "^S-\d-\d+(-\d+){1,14}(-\d+){0,1}$") { + $principal_username_sid = Convert-ToSID -account_name $principal_username_sid + } + $principal_group_sid = $task_definition_xml.Task.Principals.Principal.GroupId + if ($null -ne $principal_group_sid -and $principal_group_sid -notmatch "^S-\d-\d+(-\d+){1,14}(-\d+){0,1}$") { + $principal_group_sid = Convert-ToSID -account_name $principal_group_sid + } + + if ($null -ne $username_sid) { + $new_user_name = Convert-FromSid -sid $username_sid + if ($null -ne $principal_group_sid) { + $existing_account_name = Convert-FromSid -sid $principal_group_sid + [void]$changes.Add("-GroupId=$existing_account_name`n+UserId=$new_user_name") + $task_principal.UserId = $new_user_name + $task_principal.GroupId = $null + } + elseif ($null -eq $principal_username_sid) { + [void]$changes.Add("+UserId=$new_user_name") + $task_principal.UserId = $new_user_name + } + elseif ($principal_username_sid -ne $username_sid) { + $existing_account_name = Convert-FromSid -sid $principal_username_sid + [void]$changes.Add("-UserId=$existing_account_name`n+UserId=$new_user_name") + $task_principal.UserId = $new_user_name + } + } + if ($null -ne $group_sid) { + $new_group_name = Convert-FromSid -sid $group_sid + if ($null -ne $principal_username_sid) { + $existing_account_name = Convert-FromSid -sid $principal_username_sid + [void]$changes.Add("-UserId=$existing_account_name`n+GroupId=$new_group_name") + $task_principal.UserId = $null + $task_principal.GroupId = $new_group_name + } + elseif ($null -eq $principal_group_sid) { + [void]$changes.Add("+GroupId=$new_group_name") + $task_principal.GroupId = $new_group_name + } + elseif ($principal_group_sid -ne $group_sid) { + $existing_account_name = Convert-FromSid -sid $principal_group_sid + [void]$changes.Add("-GroupId=$existing_account_name`n+GroupId=$new_group_name") + $task_principal.GroupId = $new_group_name + } + } + + return , $changes +} + +Function Compare-RegistrationInfo($task_definition) { + # compares the RegistrationInfo property and returns a list of changed + # objects for use in a diff string + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa382100(v=vs.85).aspx + $reg_info_map = @{ + Author = $author + Date = $date + Description = $description + Source = $source + Version = $version + } + $changes = Compare-Property -property_name "RegistrationInfo" -parent_property $task_definition.RegistrationInfo -map $reg_info_map + + return , $changes +} + +Function Compare-Setting($task_definition) { + # compares the task Settings property and returns a list of changed objects + # for use in a diff string + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa383480(v=vs.85).aspx + $settings_map = @{ + AllowDemandStart = $allow_demand_start + AllowHardTerminate = $allow_hard_terminate + Compatibility = $compatibility + DeleteExpiredTaskAfter = $delete_expired_task_after + DisallowStartIfOnBatteries = $disallow_start_if_on_batteries + ExecutionTimeLimit = $execution_time_limit + Enabled = $enabled + Hidden = $hidden + # IdleSettings = $idle_settings # TODO: this takes in a COM object + MultipleInstances = $multiple_instances + # NetworkSettings = $network_settings # TODO: this takes in a COM object + Priority = $priority + RestartCount = $restart_count + RestartInterval = $restart_interval + RunOnlyIfIdle = $run_only_if_idle + RunOnlyIfNetworkAvailable = $run_only_if_network_available + StartWhenAvailable = $start_when_available + StopIfGoingOnBatteries = $stop_if_going_on_batteries + WakeToRun = $wake_to_run + } + $changes = Compare-Property -property_name "Settings" -parent_property $task_definition.Settings -map $settings_map + + return , $changes +} + +Function Compare-Trigger($task_definition) { + # compares the task Triggers property and returns a list of changed objects + # for use in a diff string + # TriggerCollection - https://msdn.microsoft.com/en-us/library/windows/desktop/aa383875(v=vs.85).aspx + # Trigger - https://msdn.microsoft.com/en-us/library/windows/desktop/aa383868(v=vs.85).aspx + if ($null -eq $triggers) { + return , [System.Collections.ArrayList]@() + } + + $task_triggers = $task_definition.Triggers + $existing_count = $task_triggers.Count + + # because we clear the actions and re-add them to keep the order, we need + # to convert the existing actions to a new list. + # The Item property in actions starts at 1 + $existing_triggers = [System.Collections.ArrayList]@() + for ($i = 1; $i -le $existing_count; $i++) { + [void]$existing_triggers.Add($task_triggers.Item($i)) + } + if ($existing_count -gt 0) { + $task_triggers.Clear() + } + + $map = @{ + [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_BOOT = @{ + mandatory = @() + optional = @('delay', 'enabled', 'end_boundary', 'execution_time_limit', 'start_boundary', 'repetition') + } + [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_DAILY = @{ + mandatory = @('start_boundary') + optional = @('days_interval', 'enabled', 'end_boundary', 'execution_time_limit', 'random_delay', 'repetition') + } + [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_EVENT = @{ + mandatory = @('subscription') + # TODO: ValueQueries is a COM object + optional = @('delay', 'enabled', 'end_boundary', 'execution_time_limit', 'start_boundary', 'repetition') + } + [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_IDLE = @{ + mandatory = @() + optional = @('enabled', 'end_boundary', 'execution_time_limit', 'start_boundary', 'repetition') + } + [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_LOGON = @{ + mandatory = @() + optional = @('delay', 'enabled', 'end_boundary', 'execution_time_limit', 'start_boundary', 'user_id', 'repetition') + } + [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_MONTHLYDOW = @{ + mandatory = @('start_boundary') + # Make sure run_on_last_week_of_month comes after weeks_of_month + # https://github.com/ansible-collections/community.windows/issues/414 + optional = @('days_of_week', 'enabled', 'end_boundary', 'execution_time_limit', 'months_of_year', 'random_delay', 'weeks_of_month', + 'run_on_last_week_of_month', 'repetition') + } + [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_MONTHLY = @{ + mandatory = @('days_of_month', 'start_boundary') + optional = @('enabled', 'end_boundary', 'execution_time_limit', 'months_of_year', 'random_delay', 'run_on_last_day_of_month', + 'start_boundary', 'repetition') + } + [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_REGISTRATION = @{ + mandatory = @() + optional = @('delay', 'enabled', 'end_boundary', 'execution_time_limit', 'start_boundary', 'repetition') + } + [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_TIME = @{ + mandatory = @('start_boundary') + optional = @('enabled', 'end_boundary', 'execution_time_limit', 'random_delay', 'repetition') + } + [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_WEEKLY = @{ + mandatory = @('days_of_week', 'start_boundary') + optional = @('enabled', 'end_boundary', 'execution_time_limit', 'random_delay', 'weeks_interval', 'repetition') + } + [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_SESSION_STATE_CHANGE = @{ + mandatory = @() + optional = @('delay', 'enabled', 'end_boundary', 'execution_time_limit', 'repetition', 'start_boundary', 'state_change', 'user_id' ) + } + } + $compareParams = @{ + collection = $task_triggers + property_name = "trigger" + new = $triggers + existing = $existing_triggers + map = $map + enum = "TASK_TRIGGER_TYPE2" + } + $changes = Compare-PropertyList @compareParams + + return , $changes +} + +Function Test-TaskExist($task_folder, $name) { + # checks if a task exists in the TaskFolder COM object, returns null if the + # task does not exist, otherwise returns the RegisteredTask object + $task = $null + if ($task_folder) { + $raw_tasks = $task_folder.GetTasks(1) # 1 = TASK_ENUM_HIDDEN + + for ($i = 1; $i -le $raw_tasks.Count; $i++) { + if ($raw_tasks.Item($i).Name -eq $name) { + $task = $raw_tasks.Item($i) + break + } + } + } + + return $task +} + +Function Test-XmlDurationFormat($key, $value) { + # validate value is in the Duration Data Type format + # PnYnMnDTnHnMnS + try { + $time_span = [System.Xml.XmlConvert]::ToTimeSpan($value) + return $time_span + } + catch [System.FormatException] { + Fail-Json -obj $result -message "trigger option '$key' must be in the XML duration format but was '$value'" + } +} + +###################################### +### VALIDATION/BUILDING OF OPTIONS ### +###################################### + +# invalid characters in task name +$invalid_name_chars = '\/:*?"<>|' +$invalid_name_chars_regex = "[$([regex]::Escape($invalid_name_chars))]" + +if ($name -cmatch $invalid_name_chars_regex) { + Fail-Json -obj $result -message "Invalid task name '$name'. The following characters are not valid: $invalid_name_chars" +} + +# convert username and group to SID if set +$username_sid = $null +if ($username) { + $username_sid = Convert-ToSID -account_name $username +} +$group_sid = $null +if ($group) { + $group_sid = Convert-ToSID -account_name $group +} + +# validate store_password and logon_type +if ($null -ne $logon_type) { + $full_enum_name = "TASK_LOGON_$($logon_type.ToUpper())" + $logon_type = [TASK_LOGON_TYPE]::$full_enum_name +} + +# now validate the logon_type option with the other parameters +if ($null -ne $username -and $null -ne $group) { + Fail-Json -obj $result -message "username and group can not be set at the same time" +} +if ($null -ne $logon_type) { + if ($logon_type -eq [TASK_LOGON_TYPE]::TASK_LOGON_S4U -and $null -eq $password) { + Fail-Json -obj $result -message "password must be set when logon_type=s4u" + } + + if ($logon_type -eq [TASK_LOGON_TYPE]::TASK_LOGON_GROUP -and $null -eq $group) { + Fail-Json -obj $result -message "group must be set when logon_type=group" + } + + # SIDs == Local System, Local Service and Network Service + if ($logon_type -eq [TASK_LOGON_TYPE]::TASK_LOGON_SERVICE_ACCOUNT -and $username_sid -notin @("S-1-5-18", "S-1-5-19", "S-1-5-20")) { + Fail-Json -obj $result -message "username must be SYSTEM, LOCAL SERVICE or NETWORK SERVICE when logon_type=service_account" + } +} + +# convert the run_level to enum value +if ($null -ne $run_level) { + if ($run_level -eq "limited") { + $run_level = [TASK_RUN_LEVEL]::TASK_RUNLEVEL_LUA + } + else { + $run_level = [TASK_RUN_LEVEL]::TASK_RUNLEVEL_HIGHEST + } +} + +# manually add the only support action type for each action - also convert PSCustomObject to Hashtable +for ($i = 0; $i -lt $actions.Count; $i++) { + $action = $actions[$i] + $action.type = [TASK_ACTION_TYPE]::TASK_ACTION_EXEC + if (-not $action.ContainsKey("path")) { + Fail-Json -obj $result -message "action entry must contain the key 'path'" + } + $actions[$i] = $action +} + +# convert and validate the triggers - and convert PSCustomObject to Hashtable +for ($i = 0; $i -lt $triggers.Count; $i++) { + $trigger = $triggers[$i] + $valid_trigger_types = @('event', 'time', 'daily', 'weekly', 'monthly', 'monthlydow', 'idle', 'registration', 'boot', 'logon', 'session_state_change') + if (-not $trigger.ContainsKey("type")) { + Fail-Json -obj $result -message "a trigger entry must contain a key 'type' with a value of '$($valid_trigger_types -join "', '")'" + } + + $trigger_type = $trigger.type + if ($trigger_type -notin $valid_trigger_types) { + $msg = "the specified trigger type '$trigger_type' is not valid, type must be a value of '$($valid_trigger_types -join "', '")'" + Fail-Json -obj $result -message $msg + } + + $full_enum_name = "TASK_TRIGGER_$($trigger_type.ToUpper())" + $trigger_type = [TASK_TRIGGER_TYPE2]::$full_enum_name + $trigger.type = $trigger_type + + $date_properties = @('start_boundary', 'end_boundary') + foreach ($property_name in $date_properties) { + # validate the date is in the DateTime format + # yyyy-mm-ddThh:mm:ss + if ($trigger.ContainsKey($property_name)) { + $date_value = $trigger.$property_name + try { + $date = Get-Date -Date $date_value -Format "yyyy-MM-dd'T'HH:mm:ssK" + # make sure we convert it to the full string format + $trigger.$property_name = $date.ToString() + } + catch [System.Management.Automation.ParameterBindingException] { + Fail-Json -obj $result -message "trigger option '$property_name' must be in the format 'YYYY-MM-DDThh:mm:ss' format but was '$date_value'" + } + } + } + + $time_properties = @('execution_time_limit', 'delay', 'random_delay') + foreach ($property_name in $time_properties) { + if ($trigger.ContainsKey($property_name)) { + $time_span = $trigger.$property_name + Test-XmlDurationFormat -key $property_name -value $time_span + } + } + + if ($trigger.ContainsKey("repetition")) { + if ($trigger.repetition -is [Array]) { + # Legacy doesn't natively support deprecate by date, need to do this manually until we use Ansible.Basic + if (-not $result.ContainsKey('deprecations')) { + $result.deprecations = @() + } + $result.deprecations += @{ + msg = "repetition is a list, should be defined as a dict" + date = "2021-07-01" + collection_name = "community.windows" + } + $trigger.repetition = $trigger.repetition[0] + } + + $interval_timespan = $null + if ($trigger.repetition.ContainsKey("interval") -and $null -ne $trigger.repetition.interval) { + $interval_timespan = Test-XmlDurationFormat -key "interval" -value $trigger.repetition.interval + } + + $duration_timespan = $null + if ($trigger.repetition.ContainsKey("duration") -and $null -ne $trigger.repetition.duration) { + $duration_timespan = Test-XmlDurationFormat -key "duration" -value $trigger.repetition.duration + } + + if ($null -ne $interval_timespan -and $null -ne $duration_timespan -and $interval_timespan -gt $duration_timespan) { + $msg = -join @( + "trigger repetition option 'interval' value '$($trigger.repetition.interval)' " + "must be less than or equal to 'duration' value '$($trigger.repetition.duration)'" + ) + Fail-Json -obj $result -message $msg + } + } + + # convert out human readble text to the hex values for these properties + if ($trigger.ContainsKey("days_of_week")) { + $days = $trigger.days_of_week + if ($days -is [String]) { + $days = $days.Split(",").Trim() + } + elseif ($days -isnot [Array]) { + $days = @($days) + } + + $day_value = 0 + foreach ($day in $days) { + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa382057(v=vs.85).aspx + switch ($day) { + sunday { $day_value = $day_value -bor 0x01 } + monday { $day_value = $day_value -bor 0x02 } + tuesday { $day_value = $day_value -bor 0x04 } + wednesday { $day_value = $day_value -bor 0x08 } + thursday { $day_value = $day_value -bor 0x10 } + friday { $day_value = $day_value -bor 0x20 } + saturday { $day_value = $day_value -bor 0x40 } + default { Fail-Json -obj $result -message "invalid day of week '$day', check the spelling matches the full day name" } + } + } + if ($day_value -eq 0) { + $day_value = $null + } + + $trigger.days_of_week = $day_value + } + if ($trigger.ContainsKey("days_of_month")) { + $days = $trigger.days_of_month + if ($days -is [String]) { + $days = $days.Split(",").Trim() + } + elseif ($days -isnot [Array]) { + $days = @($days) + } + + $day_value = 0 + foreach ($day in $days) { + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa382063(v=vs.85).aspx + switch ($day) { + 1 { $day_value = $day_value -bor 0x01 } + 2 { $day_value = $day_value -bor 0x02 } + 3 { $day_value = $day_value -bor 0x04 } + 4 { $day_value = $day_value -bor 0x08 } + 5 { $day_value = $day_value -bor 0x10 } + 6 { $day_value = $day_value -bor 0x20 } + 7 { $day_value = $day_value -bor 0x40 } + 8 { $day_value = $day_value -bor 0x80 } + 9 { $day_value = $day_value -bor 0x100 } + 10 { $day_value = $day_value -bor 0x200 } + 11 { $day_value = $day_value -bor 0x400 } + 12 { $day_value = $day_value -bor 0x800 } + 13 { $day_value = $day_value -bor 0x1000 } + 14 { $day_value = $day_value -bor 0x2000 } + 15 { $day_value = $day_value -bor 0x4000 } + 16 { $day_value = $day_value -bor 0x8000 } + 17 { $day_value = $day_value -bor 0x10000 } + 18 { $day_value = $day_value -bor 0x20000 } + 19 { $day_value = $day_value -bor 0x40000 } + 20 { $day_value = $day_value -bor 0x80000 } + 21 { $day_value = $day_value -bor 0x100000 } + 22 { $day_value = $day_value -bor 0x200000 } + 23 { $day_value = $day_value -bor 0x400000 } + 24 { $day_value = $day_value -bor 0x800000 } + 25 { $day_value = $day_value -bor 0x1000000 } + 26 { $day_value = $day_value -bor 0x2000000 } + 27 { $day_value = $day_value -bor 0x4000000 } + 28 { $day_value = $day_value -bor 0x8000000 } + 29 { $day_value = $day_value -bor 0x10000000 } + 30 { $day_value = $day_value -bor 0x20000000 } + 31 { $day_value = $day_value -bor 0x40000000 } + default { Fail-Json -obj $result -message "invalid day of month '$day', please specify numbers from 1-31" } + } + } + if ($day_value -eq 0) { + $day_value = $null + } + $trigger.days_of_month = $day_value + } + if ($trigger.ContainsKey("weeks_of_month")) { + $weeks = $trigger.weeks_of_month + if ($weeks -is [String]) { + $weeks = $weeks.Split(",").Trim() + } + elseif ($weeks -isnot [Array]) { + $weeks = @($weeks) + } + + $week_value = 0 + foreach ($week in $weeks) { + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa382061(v=vs.85).aspx + switch ($week) { + 1 { $week_value = $week_value -bor 0x01 } + 2 { $week_value = $week_value -bor 0x02 } + 3 { $week_value = $week_value -bor 0x04 } + 4 { $week_value = $week_value -bor 0x08 } + default { Fail-Json -obj $result -message "invalid week of month '$week', please specify weeks from 1-4" } + } + + } + if ($week_value -eq 0) { + $week_value = $null + } + $trigger.weeks_of_month = $week_value + } + if ($trigger.ContainsKey("months_of_year")) { + $months = $trigger.months_of_year + if ($months -is [String]) { + $months = $months.Split(",").Trim() + } + elseif ($months -isnot [Array]) { + $months = @($months) + } + + $month_value = 0 + foreach ($month in $months) { + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa382064(v=vs.85).aspx + switch ($month) { + january { $month_value = $month_value -bor 0x01 } + february { $month_value = $month_value -bor 0x02 } + march { $month_value = $month_value -bor 0x04 } + april { $month_value = $month_value -bor 0x08 } + may { $month_value = $month_value -bor 0x10 } + june { $month_value = $month_value -bor 0x20 } + july { $month_value = $month_value -bor 0x40 } + august { $month_value = $month_value -bor 0x80 } + september { $month_value = $month_value -bor 0x100 } + october { $month_value = $month_value -bor 0x200 } + november { $month_value = $month_value -bor 0x400 } + december { $month_value = $month_value -bor 0x800 } + default { Fail-Json -obj $result -message "invalid month name '$month', please specify full month name" } + } + } + if ($month_value -eq 0) { + $month_value = $null + } + $trigger.months_of_year = $month_value + } + if ($trigger.ContainsKey("state_change")) { + $trigger.state_change = switch ($trigger.state_change) { + console_connect { [TASK_SESSION_STATE_CHANGE_TYPE]::TASK_CONSOLE_CONNECT } + console_disconnect { [TASK_SESSION_STATE_CHANGE_TYPE]::TASK_CONSOLE_DISCONNECT } + remote_connect { [TASK_SESSION_STATE_CHANGE_TYPE]::TASK_REMOTE_CONNECT } + remote_disconnect { [TASK_SESSION_STATE_CHANGE_TYPE]::TASK_REMOTE_DISCONNECT } + session_lock { [TASK_SESSION_STATE_CHANGE_TYPE]::TASK_SESSION_LOCK } + session_unlock { [TASK_SESSION_STATE_CHANGE_TYPE]::TASK_SESSION_UNLOCK } + default { + Fail-Json -obj $result -message "invalid state_change '$($trigger.state_change)'" + } + } + } + $triggers[$i] = $trigger +} + +# add \ to start of path if it is not already there +if (-not $path.StartsWith("\")) { + $path = "\$path" +} +# ensure path does not end with \ if more than 1 char +if ($path.EndsWith("\") -and $path.Length -ne 1) { + $path = $path.Substring(0, $path.Length - 1) +} + +######################## +### START CODE BLOCK ### +######################## +$service = New-Object -ComObject Schedule.Service +try { + $service.Connect() +} +catch { + Fail-Json -obj $result -message "failed to connect to the task scheduler service: $($_.Exception.Message)" +} + +# check that the path for the task set exists, create if need be +try { + $task_folder = $service.GetFolder($path) +} +catch { + $task_folder = $null +} + +# try and get the task at the path +$task = Test-TaskExist -task_folder $task_folder -name $name +$task_path = Join-Path -Path $path -ChildPath $name + +if ($state -eq "absent") { + if ($null -ne $task) { + if (-not $check_mode) { + try { + $task_folder.DeleteTask($name, 0) + } + catch { + Fail-Json -obj $result -message "failed to delete task '$name' at path '$path': $($_.Exception.Message)" + } + } + if ($diff_mode) { + $result.diff.prepared = "-[Task]`n-$task_path`n" + } + $result.changed = $true + + # check if current folder has any more tasks + $other_tasks = $task_folder.GetTasks(1) # 1 = TASK_ENUM_HIDDEN + if ($other_tasks.Count -eq 0 -and $task_folder.Name -ne "\") { + try { + $task_folder.DeleteFolder($null, $null) + } + catch { + Fail-Json -obj $result -message "failed to delete empty task folder '$path' after task deletion: $($_.Exception.Message)" + } + } + } +} +else { + if ($null -eq $task) { + $create_diff_string = "+[Task]`n+$task_path`n`n" + # to create a bare minimum task we need 1 action + if ($null -eq $actions -or $actions.Count -eq 0) { + Fail-Json -obj $result -message "cannot create a task with no actions, set at least one action with a path to an executable" + } + + # Create a bare minimum task here, further properties will be set later on + $task_definition = $service.NewTask(0) + + # Set Actions info + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa446803(v=vs.85).aspx + $create_diff_string += "[Actions]`n" + $task_actions = $task_definition.Actions + foreach ($action in $actions) { + $create_diff_string += "+action[0] = {`n +Type=$([TASK_ACTION_TYPE]::TASK_ACTION_EXEC),`n +Path=$($action.path)`n" + $task_action = $task_actions.Create([TASK_ACTION_TYPE]::TASK_ACTION_EXEC) + $task_action.Path = $action.path + if ($null -ne $action.arguments) { + $create_diff_string += " +Arguments=$($action.arguments)`n" + $task_action.Arguments = $action.arguments + } + if ($null -ne $action.working_directory) { + $create_diff_string += " +WorkingDirectory=$($action.working_directory)`n" + $task_action.WorkingDirectory = $action.working_directory + } + $create_diff_string += "+}`n" + } + + # Register the new task + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa382577(v=vs.85).aspx + if ($check_mode) { + # Only validate the task in check mode + $task_creation_flags = [TASK_CREATION]::TASK_VALIDATE_ONLY + } + else { + # Create the task but do not fire it as we still need to configure it further below + $task_creation_flags = [TASK_CREATION]::TASK_CREATE -bor [TASK_CREATION]::TASK_IGNORE_REGISTRATION_TRIGGERS + } + + # folder doesn't exist, need to create + if ($null -eq $task_folder) { + $task_folder = $service.GetFolder("\") + try { + if (-not $check_mode) { + $task_folder = $task_folder.CreateFolder($path) + } + } + catch { + Fail-Json -obj $result -message "failed to create new folder at path '$path': $($_.Exception.Message)" + } + } + + try { + $task = $task_folder.RegisterTaskDefinition($name, $task_definition, $task_creation_flags, $null, $null, $null) + } + catch { + Fail-Json -obj $result -message "failed to register new task definition: $($_.Exception.Message)" + } + if ($diff_mode) { + $result.diff.prepared = $create_diff_string + } + + $result.changed = $true + } + + # we cannot configure a task that was created above in check mode as it + # won't actually exist + if ($task) { + $task_definition = $task.Definition + $task_definition_xml = [xml]$task_definition.XmlText + + $action_changes = Compare-Action -task_definition $task_definition + $principal_changed = Compare-Principal -task_definition $task_definition -task_definition_xml $task_definition_xml + $reg_info_changed = Compare-RegistrationInfo -task_definition $task_definition + $settings_changed = Compare-Setting -task_definition $task_definition + $trigger_changes = Compare-Trigger -task_definition $task_definition + + # compile the diffs into one list with headers + $task_diff = [System.Collections.ArrayList]@() + if ($action_changes.Count -gt 0) { + [void]$task_diff.Add("[Actions]") + foreach ($action_change in $action_changes) { + [void]$task_diff.Add($action_change) + } + [void]$task_diff.Add("`n") + } + if ($principal_changed.Count -gt 0) { + [void]$task_diff.Add("[Principal]") + foreach ($principal_change in $principal_changed) { + [void]$task_diff.Add($principal_change) + } + [void]$task_diff.Add("`n") + } + if ($reg_info_changed.Count -gt 0) { + [void]$task_diff.Add("[Registration Info]") + foreach ($reg_info_change in $reg_info_changed) { + [void]$task_diff.Add($reg_info_change) + } + [void]$task_diff.Add("`n") + } + if ($settings_changed.Count -gt 0) { + [void]$task_diff.Add("[Settings]") + foreach ($settings_change in $settings_changed) { + [void]$task_diff.add($settings_change) + } + [void]$task_diff.Add("`n") + } + if ($trigger_changes.Count -gt 0) { + [void]$task_diff.Add("[Triggers]") + foreach ($trigger_change in $trigger_changes) { + [void]$task_diff.Add("$trigger_change") + } + [void]$task_diff.Add("`n") + } + + if ($null -ne $password -and (($update_password -eq $true) -or ($task_diff.Count -gt 0))) { + # because we can't compare the passwords we just need to reset it + $register_username = $username + $register_password = $password + $register_logon_type = $task_principal.LogonType + } + else { + # will inherit from the Principal property values + $register_username = $null + $register_password = $null + $register_logon_type = $null + } + + if ($task_diff.Count -gt 0 -or $null -ne $register_password) { + if ($check_mode) { + # Only validate the task in check mode + $task_creation_flags = [TASK_CREATION]::TASK_VALIDATE_ONLY + } + else { + # Create the task + $task_creation_flags = [TASK_CREATION]::TASK_CREATE_OR_UPDATE + } + try { + $task_folder.RegisterTaskDefinition( + $name, + $task_definition, + $task_creation_flags, + $register_username, + $register_password, + $register_logon_type + ) | Out-Null + } + catch { + Fail-Json -obj $result -message "failed to modify scheduled task: $($_.Exception.Message)" + } + + $result.changed = $true + + if ($diff_mode) { + $changed_diff_text = $task_diff -join "`n" + if ($null -ne $result.diff.prepared) { + $diff_text = "$($result.diff.prepared)`n$changed_diff_text" + } + else { + $diff_text = $changed_diff_text + } + $result.diff.prepared = $diff_text.Trim() + } + } + } +} + +Exit-Json -obj $result diff --git a/ansible_collections/community/windows/plugins/modules/win_scheduled_task.py b/ansible_collections/community/windows/plugins/modules/win_scheduled_task.py new file mode 100644 index 000000000..d43089b83 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_scheduled_task.py @@ -0,0 +1,539 @@ +#!/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) + +DOCUMENTATION = r''' +--- +module: win_scheduled_task +short_description: Manage scheduled tasks +description: +- Creates/modifies or removes Windows scheduled tasks. +options: + # module definition options + name: + description: + - The name of the scheduled task without the path. + type: str + required: yes + path: + description: + - Task folder in which this task will be stored. + - Will create the folder when C(state=present) and the folder does not + already exist. + - Will remove the folder when C(state=absent) and there are no tasks left + in the folder. + type: str + default: \ + state: + description: + - When C(state=present) will ensure the task exists. + - When C(state=absent) will ensure the task does not exist. + type: str + choices: [ absent, present ] + default: present + + # Action options + actions: + description: + - A list of action to configure for the task. + - See suboptions for details on how to construct each list entry. + - When creating a task there MUST be at least one action but when deleting + a task this can be a null or an empty list. + - The ordering of this list is important, the module will ensure the order + is kept when modifying the task. + - This module only supports the C(ExecAction) type but can still delete the + older legacy types. + type: list + elements: dict + suboptions: + path: + description: + - The path to the executable for the ExecAction. + type: str + required: yes + arguments: + description: + - An argument string to supply for the executable. + type: str + working_directory: + description: + - The working directory to run the executable from. + type: str + + # Trigger options + triggers: + description: + - A list of triggers to configure for the task. + - See suboptions for details on how to construct each list entry. + - The ordering of this list is important, the module will ensure the order + is kept when modifying the task. + - There are multiple types of triggers, see U(https://msdn.microsoft.com/en-us/library/windows/desktop/aa383868.aspx) + for a list of trigger types and their options. + - The suboption options listed below are not required for all trigger + types, read the description for more details. + type: list + elements: dict + suboptions: + type: + description: + - The trigger type, this value controls what below options are + required. + type: str + required: yes + choices: [ boot, daily, event, idle, logon, monthlydow, monthly, registration, time, weekly, session_state_change ] + enabled: + description: + - Whether to set the trigger to enabled or disabled + - Used in all trigger types. + type: bool + start_boundary: + description: + - The start time for the task, even if the trigger meets the other + start criteria, it won't start until this time is met. + - If you wish to run a task at 9am on a day you still need to specify + the date on which the trigger is activated, you can set any date even + ones in the past. + - Required when C(type) is C(daily), C(monthlydow), C(monthly), + C(time), C(weekly). + - Optional for the rest of the trigger types. + - This is in ISO 8601 DateTime format C(YYYY-MM-DDThh:mm:ss). + type: str + end_boundary: + description: + - The end time for when the trigger is deactivated. + - This is in ISO 8601 DateTime format C(YYYY-MM-DDThh:mm:ss). + type: str + execution_time_limit: + description: + - The maximum amount of time that the task is allowed to run for. + - Optional for all the trigger types. + - Is in the ISO 8601 Duration format C(P[n]Y[n]M[n]DT[n]H[n]M[n]S). + type: str + delay: + description: + - The time to delay the task from running once the trigger has been + fired. + - Optional when C(type) is C(boot), C(event), C(logon), + C(registration), C(session_state_change). + - Is in the ISO 8601 Duration format C(P[n]Y[n]M[n]DT[n]H[n]M[n]S). + type: str + random_delay: + description: + - The delay time that is randomly added to the start time of the + trigger. + - Optional when C(type) is C(daily), C(monthlydow), C(monthly), + C(time), C(weekly). + - Is in the ISO 8601 Duration format C(P[n]Y[n]M[n]DT[n]H[n]M[n]S). + type: str + subscription: + description: + - Only used and is required for C(type=event). + - The XML query string that identifies the event that fires the + trigger. + type: str + user_id: + description: + - The username that the trigger will target. + - Optional when C(type) is C(logon), C(session_state_change). + - Can be the username or SID of a user. + - When C(type=logon) and you want the trigger to fire when a user in a + group logs on, leave this as null and set C(group) to the group you + wish to trigger. + type: str + days_of_week: + description: + - The days of the week for the trigger. + - Can be a list or comma separated string of full day names e.g. monday + instead of mon. + - Required when C(type) is C(weekly). + - Optional when C(type=monthlydow). + type: str + days_of_month: + description: + - The days of the month from 1 to 31 for the triggers. + - If you wish to set the trigger for the last day of any month + use C(run_on_last_day_of_month). + - Can be a list or comma separated string of day numbers. + - Required when C(type=monthly). + type: str + weeks_of_month: + description: + - The weeks of the month for the trigger. + - Can be a list or comma separated string of the numbers 1 to 4 + representing the first to 4th week of the month. + - Optional when C(type=monthlydow). + type: str + months_of_year: + description: + - The months of the year for the trigger. + - Can be a list or comma separated string of full month names e.g. + march instead of mar. + - Optional when C(type) is C(monthlydow), C(monthly). + type: str + run_on_last_week_of_month: + description: + - Boolean value that sets whether the task runs on the last week of the + month. + - Optional when C(type) is C(monthlydow). + type: bool + run_on_last_day_of_month: + description: + - Boolean value that sets whether the task runs on the last day of the + month. + - Optional when C(type) is C(monthly). + type: bool + weeks_interval: + description: + - The interval of weeks to run on, e.g. C(1) means every week while + C(2) means every other week. + - Optional when C(type=weekly). + type: int + repetition: + description: + - Allows you to define the repetition action of the trigger that defines how often the task is run and how long the repetition pattern is repeated + after the task is started. + - It takes in the following keys, C(duration), C(interval), C(stop_at_duration_end) + suboptions: + duration: + description: + - Defines how long the pattern is repeated. + - The value is in the ISO 8601 Duration format C(P[n]Y[n]M[n]DT[n]H[n]M[n]S). + - By default this is not set which means it will repeat indefinitely. + type: str + interval: + description: + - The amount of time between each restart of the task. + - The value is written in the ISO 8601 Duration format C(P[n]Y[n]M[n]DT[n]H[n]M[n]S). + type: str + stop_at_duration_end: + description: + - Whether a running instance of the task is stopped at the end of the repetition pattern. + type: bool + state_change: + description: + - Allows you to define the kind of Terminal Server session change that triggers a task. + - Optional when C(type=session_state_change) + type: str + choices: + - console_connect + - console_disconnect + - remote_connect + - remote_disconnect + - session_lock + - session_unlock + version_added: 1.6.0 + + # Principal options + display_name: + description: + - The name of the user/group that is displayed in the Task Scheduler UI. + type: str + group: + description: + - The group that will run the task. + - C(group) and C(username) are exclusive to each other and cannot be set + at the same time. + - C(logon_type) can either be not set or equal C(group). + type: str + logon_type: + description: + - The logon method that the task will run with. + - C(password) means the password will be stored and the task has access + to network resources. + - C(s4u) means the existing token will be used to run the task and no + password will be stored with the task. Means no network or encrypted + files access. + - C(interactive_token) means the user must already be logged on + interactively and will run in an existing interactive session. + - C(group) means that the task will run as a group. + - C(service_account) means that a service account like System, Local + Service or Network Service will run the task. + type: str + choices: [ none, password, s4u, interactive_token, group, service_account, token_or_password ] + run_level: + description: + - The level of user rights used to run the task. + - If not specified the task will be created with limited rights. + type: str + choices: [ limited, highest ] + aliases: [ runlevel ] + username: + description: + - The user to run the scheduled task as. + - Will default to the current user under an interactive token if not + specified during creation. + - The user account specified must have the C(SeBatchLogonRight) logon right + which can be added with M(ansible.windows.win_user_right). + type: str + aliases: [ user ] + password: + description: + - The password for the user account to run the scheduled task as. + - This is required when running a task without the user being logged in, + excluding the builtin service accounts and Group Managed Service Accounts (gMSA). + - If set, will always result in a change unless C(update_password) is set + to C(no) and no other changes are required for the service. + type: str + update_password: + description: + - Whether to update the password even when not other changes have occurred. + - When C(yes) will always result in a change when executing the module. + type: bool + default: yes + + # RegistrationInfo options + author: + description: + - The author of the task. + type: str + date: + description: + - The date when the task was registered. + type: str + description: + description: + - The description of the task. + type: str + source: + description: + - The source of the task. + type: str + version: + description: + - The version number of the task. + type: str + + # Settings options + allow_demand_start: + description: + - Whether the task can be started by using either the Run command or the + Context menu. + type: bool + allow_hard_terminate: + description: + - Whether the task can be terminated by using TerminateProcess. + type: bool + compatibility: + description: + - The integer value with indicates which version of Task Scheduler a task + is compatible with. + - C(0) means the task is compatible with the AT command. + - C(1) means the task is compatible with Task Scheduler 1.0(Windows Vista, Windows Server 2008 and older). + - C(2) means the task is compatible with Task Scheduler 2.0(Windows Vista, Windows Server 2008). + - C(3) means the task is compatible with Task Scheduler 2.0(Windows 7, Windows Server 2008 R2). + - C(4) means the task is compatible with Task Scheduler 2.0(Windows 10, Windows Server 2016, Windows Server 2019). + type: int + choices: [ 0, 1, 2, 3, 4 ] + delete_expired_task_after: + description: + - The amount of time that the Task Scheduler will wait before deleting the + task after it expires. + - A task expires after the end_boundary has been exceeded for all triggers + associated with the task. + - This is in the ISO 8601 Duration format C(P[n]Y[n]M[n]DT[n]H[n]M[n]S). + type: str + disallow_start_if_on_batteries: + description: + - Whether the task will not be started if the computer is running on + battery power. + type: bool + enabled: + description: + - Whether the task is enabled, the task can only run when C(yes). + type: bool + execution_time_limit: + description: + - The amount of time allowed to complete the task. + - When set to C(PT0S), the time limit is infinite. + - When omitted, the default time limit is 72 hours. + - This is in the ISO 8601 Duration format C(P[n]Y[n]M[n]DT[n]H[n]M[n]S). + type: str + hidden: + description: + - Whether the task will be hidden in the UI. + type: bool + multiple_instances: + description: + - An integer that indicates the behaviour when starting a task that is + already running. + - C(0) will start a new instance in parallel with existing instances of + that task. + - C(1) will wait until other instances of that task to finish running + before starting itself. + - C(2) will not start a new instance if another is running. + - C(3) will stop other instances of the task and start the new one. + type: int + choices: [ 0, 1, 2, 3 ] + priority: + description: + - The priority level (0-10) of the task. + - When creating a new task the default is C(7). + - See U(https://msdn.microsoft.com/en-us/library/windows/desktop/aa383512.aspx) + for details on the priority levels. + type: int + restart_count: + description: + - The number of times that the Task Scheduler will attempt to restart the + task. + type: int + restart_interval: + description: + - How long the Task Scheduler will attempt to restart the task. + - If this is set then C(restart_count) must also be set. + - The maximum allowed time is 31 days. + - The minimum allowed time is 1 minute. + - This is in the ISO 8601 Duration format C(P[n]Y[n]M[n]DT[n]H[n]M[n]S). + type: str + run_only_if_idle: + description: + - Whether the task will run the task only if the computer is in an idle + state. + type: bool + run_only_if_network_available: + description: + - Whether the task will run only when a network is available. + type: bool + start_when_available: + description: + - Whether the task can start at any time after its scheduled time has + passed. + type: bool + stop_if_going_on_batteries: + description: + - Whether the task will be stopped if the computer begins to run on battery + power. + type: bool + wake_to_run: + description: + - Whether the task will wake the computer when it is time to run the task. + type: bool +notes: +- The option names and structure for actions and triggers of a service follow + the C(RegisteredTask) naming standard and requirements, it would be useful to + read up on this guide if coming across any issues U(https://msdn.microsoft.com/en-us/library/windows/desktop/aa382542.aspx). +- A Group Managed Service Account (gMSA) can be used by setting C(logon_type) to C(password) + and omitting the password parameter. For more information on gMSAs, + see U(https://techcommunity.microsoft.com/t5/Core-Infrastructure-and-Security/Windows-Server-2012-Group-Managed-Service-Accounts/ba-p/255910) +seealso: +- module: community.windows.win_scheduled_task_stat +- module: ansible.windows.win_user_right +author: +- Peter Mounce (@petemounce) +- Jordan Borean (@jborean93) +''' + +EXAMPLES = r''' +- name: Create a task to open 2 command prompts as SYSTEM + community.windows.win_scheduled_task: + name: TaskName + description: open command prompt + actions: + - path: cmd.exe + arguments: /c hostname + - path: cmd.exe + arguments: /c whoami + triggers: + - type: daily + start_boundary: '2017-10-09T09:00:00' + username: SYSTEM + state: present + enabled: yes + +- name: Create task to run a PS script as NETWORK service on boot + community.windows.win_scheduled_task: + name: TaskName2 + description: Run a PowerShell script + actions: + - path: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe + arguments: -ExecutionPolicy Unrestricted -NonInteractive -File C:\TestDir\Test.ps1 + triggers: + - type: boot + username: NETWORK SERVICE + run_level: highest + state: present + +- name: Update Local Security Policy to allow users to run scheduled tasks + ansible.windows.win_user_right: + name: SeBatchLogonRight + users: + - LocalUser + - DOMAIN\NetworkUser + action: add + +- name: Change above task to run under a domain user account, storing the passwords + community.windows.win_scheduled_task: + name: TaskName2 + username: DOMAIN\User + password: Password + logon_type: password + +- name: Change the above task again, choosing not to store the password + community.windows.win_scheduled_task: + name: TaskName2 + username: DOMAIN\User + logon_type: s4u + +- name: Change above task to use a gMSA, where the password is managed automatically + community.windows.win_scheduled_task: + name: TaskName2 + username: DOMAIN\gMsaSvcAcct$ + logon_type: password + +- name: Create task with multiple triggers + community.windows.win_scheduled_task: + name: TriggerTask + path: \Custom + actions: + - path: cmd.exe + triggers: + - type: daily + - type: monthlydow + username: SYSTEM + +- name: Set logon type to password but don't force update the password + community.windows.win_scheduled_task: + name: TriggerTask + path: \Custom + actions: + - path: cmd.exe + username: Administrator + password: password + update_password: no + +- name: Disable a task that already exists + community.windows.win_scheduled_task: + name: TaskToDisable + enabled: no + +- name: Create a task that will be repeated every minute for five minutes + community.windows.win_scheduled_task: + name: RepeatedTask + description: open command prompt + actions: + - path: cmd.exe + arguments: /c hostname + triggers: + - type: registration + repetition: + interval: PT1M + duration: PT5M + stop_at_duration_end: yes + +- name: Create task to run a PS script in Windows 10 compatibility on boot with a delay of 1min + community.windows.win_scheduled_task: + name: TriggerTask + path: \Custom + actions: + - path: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe + arguments: -ExecutionPolicy Unrestricted -NonInteractive -File C:\TestDir\Test.ps1 + triggers: + - type: boot + delay: PT1M + username: SYSTEM + compatibility: 4 +''' + +RETURN = r''' +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_scheduled_task_stat.ps1 b/ansible_collections/community/windows/plugins/modules/win_scheduled_task_stat.ps1 new file mode 100644 index 000000000..eb119ae05 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_scheduled_task_stat.ps1 @@ -0,0 +1,398 @@ +#!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 -PowerShell Ansible.ModuleUtils.AddType +#AnsibleRequires -CSharpUtil Ansible.Basic +#Requires -Module Ansible.ModuleUtils.CamelConversion + +$spec = @{ + options = @{ + path = @{ type = "str"; default = "\" } + name = @{ type = "str" } + } + supports_check_mode = $true +} +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$path = $module.Params.path +$name = $module.Params.name + +Function ConvertTo-NormalizedUser { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [String]$InputObject + ) + + $candidates = @(if ($InputObject.Contains('\')) { + $nameSplit = $InputObject.Split('\', 2) + + if ($nameSplit[0] -eq '.') { + # If the domain portion is . try using the hostname then falling back to just the username. + # Usually the hostname just works except when running on a DC where it's a domain account + # where looking up just the username should work. + , @($env:COMPUTERNAME, $nameSplit[1]) + $nameSplit[1] + } + else { + , $nameSplit + } + } + else { + $InputObject + }) + + $sid = for ($i = 0; $i -lt $candidates.Length; $i++) { + $candidate = $candidates[$i] + $ntAccount = New-Object -TypeName System.Security.Principal.NTAccount -ArgumentList $candidate + try { + $ntAccount.Translate([System.Security.Principal.SecurityIdentifier]) + break + } + catch [System.Security.Principal.IdentityNotMappedException] { + if ($i -eq ($candidates.Length - 1)) { + throw + } + continue + } + } + + $sid.Translate([System.Security.Principal.NTAccount]).Value +} + +Add-CSharpType -AnsibleModule $module -References @' +public enum TASK_ACTION_TYPE +{ + TASK_ACTION_EXEC = 0, + // The below are not supported and are only kept for documentation purposes + TASK_ACTION_COM_HANDLER = 5, + TASK_ACTION_SEND_EMAIL = 6, + TASK_ACTION_SHOW_MESSAGE = 7 +} + +public enum TASK_LOGON_TYPE +{ + TASK_LOGON_NONE = 0, + TASK_LOGON_PASSWORD = 1, + TASK_LOGON_S4U = 2, + TASK_LOGON_INTERACTIVE_TOKEN = 3, + TASK_LOGON_GROUP = 4, + TASK_LOGON_SERVICE_ACCOUNT = 5, + TASK_LOGON_INTERACTIVE_TOKEN_OR_PASSWORD = 6 +} + +public enum TASK_RUN_LEVEL +{ + TASK_RUNLEVEL_LUA = 0, + TASK_RUNLEVEL_HIGHEST = 1 +} + +public enum TASK_STATE +{ + TASK_STATE_UNKNOWN = 0, + TASK_STATE_DISABLED = 1, + TASK_STATE_QUEUED = 2, + TASK_STATE_READY = 3, + TASK_STATE_RUNNING = 4 +} + +public enum TASK_TRIGGER_TYPE2 +{ + TASK_TRIGGER_EVENT = 0, + TASK_TRIGGER_TIME = 1, + TASK_TRIGGER_DAILY = 2, + TASK_TRIGGER_WEEKLY = 3, + TASK_TRIGGER_MONTHLY = 4, + TASK_TRIGGER_MONTHLYDOW = 5, + TASK_TRIGGER_IDLE = 6, + TASK_TRIGGER_REGISTRATION = 7, + TASK_TRIGGER_BOOT = 8, + TASK_TRIGGER_LOGON = 9, + TASK_TRIGGER_SESSION_STATE_CHANGE = 11 +} + +public enum TASK_SESSION_STATE_CHANGE_TYPE +{ + TASK_CONSOLE_CONNECT = 1, + TASK_CONSOLE_DISCONNECT = 2, + TASK_REMOTE_CONNECT = 3, + TASK_REMOTE_DISCONNECT = 4, + TASK_SESSION_LOCK = 7, + TASK_SESSION_UNLOCK = 8 +} +'@ + +Function Get-PropertyValue($task_property, $com, $property) { + $raw_value = $com.$property + + if ($null -eq $raw_value) { + return $null + } + elseif ($raw_value.GetType().Name -eq "__ComObject") { + $com_values = @{} + Get-Member -InputObject $raw_value -MemberType Property | ForEach-Object { + $com_value = Get-PropertyValue -task_property $property -com $raw_value -property $_.Name + $com_values.$($_.Name) = $com_value + } + + return , $com_values + } + + switch ($property) { + DaysOfWeek { + $value_list = @() + $map = @( + @{ day = "sunday"; bitwise = 0x01 } + @{ day = "monday"; bitwise = 0x02 } + @{ day = "tuesday"; bitwise = 0x04 } + @{ day = "wednesday"; bitwise = 0x08 } + @{ day = "thursday"; bitwise = 0x10 } + @{ day = "friday"; bitwise = 0x20 } + @{ day = "saturday"; bitwise = 0x40 } + ) + foreach ($entry in $map) { + $day = $entry.day + $bitwise = $entry.bitwise + if ($raw_value -band $bitwise) { + $value_list += $day + } + } + + $value = $value_list -join "," + break + } + DaysOfMonth { + $value_list = @() + $map = @( + @{ day = "1"; bitwise = 0x01 } + @{ day = "2"; bitwise = 0x02 } + @{ day = "3"; bitwise = 0x04 } + @{ day = "4"; bitwise = 0x08 } + @{ day = "5"; bitwise = 0x10 } + @{ day = "6"; bitwise = 0x20 } + @{ day = "7"; bitwise = 0x40 } + @{ day = "8"; bitwise = 0x80 } + @{ day = "9"; bitwise = 0x100 } + @{ day = "10"; bitwise = 0x200 } + @{ day = "11"; bitwise = 0x400 } + @{ day = "12"; bitwise = 0x800 } + @{ day = "13"; bitwise = 0x1000 } + @{ day = "14"; bitwise = 0x2000 } + @{ day = "15"; bitwise = 0x4000 } + @{ day = "16"; bitwise = 0x8000 } + @{ day = "17"; bitwise = 0x10000 } + @{ day = "18"; bitwise = 0x20000 } + @{ day = "19"; bitwise = 0x40000 } + @{ day = "20"; bitwise = 0x80000 } + @{ day = "21"; bitwise = 0x100000 } + @{ day = "22"; bitwise = 0x200000 } + @{ day = "23"; bitwise = 0x400000 } + @{ day = "24"; bitwise = 0x800000 } + @{ day = "25"; bitwise = 0x1000000 } + @{ day = "26"; bitwise = 0x2000000 } + @{ day = "27"; bitwise = 0x4000000 } + @{ day = "28"; bitwise = 0x8000000 } + @{ day = "29"; bitwise = 0x10000000 } + @{ day = "30"; bitwise = 0x20000000 } + @{ day = "31"; bitwise = 0x40000000 } + ) + + foreach ($entry in $map) { + $day = $entry.day + $bitwise = $entry.bitwise + if ($raw_value -band $bitwise) { + $value_list += $day + } + } + + $value = $value_list -join "," + break + } + WeeksOfMonth { + $value_list = @() + $map = @( + @{ week = "1"; bitwise = 0x01 } + @{ week = "2"; bitwise = 0x02 } + @{ week = "3"; bitwise = 0x04 } + @{ week = "4"; bitwise = 0x04 } + ) + + foreach ($entry in $map) { + $week = $entry.week + $bitwise = $entry.bitwise + if ($raw_value -band $bitwise) { + $value_list += $week + } + } + + $value = $value_list -join "," + break + } + MonthsOfYear { + $value_list = @() + $map = @( + @{ month = "january"; bitwise = 0x01 } + @{ month = "february"; bitwise = 0x02 } + @{ month = "march"; bitwise = 0x04 } + @{ month = "april"; bitwise = 0x08 } + @{ month = "may"; bitwise = 0x10 } + @{ month = "june"; bitwise = 0x20 } + @{ month = "july"; bitwise = 0x40 } + @{ month = "august"; bitwise = 0x80 } + @{ month = "september"; bitwise = 0x100 } + @{ month = "october"; bitwise = 0x200 } + @{ month = "november"; bitwise = 0x400 } + @{ month = "december"; bitwise = 0x800 } + ) + + foreach ($entry in $map) { + $month = $entry.month + $bitwise = $entry.bitwise + if ($raw_value -band $bitwise) { + $value_list += $month + } + } + + $value = $value_list -join "," + break + } + Type { + if ($task_property -eq "actions") { + $value = [Enum]::ToObject([TASK_ACTION_TYPE], $raw_value).ToString() + } + elseif ($task_property -eq "triggers") { + $value = [Enum]::ToObject([TASK_TRIGGER_TYPE2], $raw_value).ToString() + } + break + } + RunLevel { + $value = [Enum]::ToObject([TASK_RUN_LEVEL], $raw_value).ToString() + break + } + LogonType { + $value = [Enum]::ToObject([TASK_LOGON_TYPE], $raw_value).ToString() + break + } + UserId { + $value = ConvertTo-NormalizedUser -InputObject $raw_value + } + GroupId { + $value = ConvertTo-NormalizedUser -InputObject $raw_value + } + default { + $value = $raw_value + break + } + } + + return , $value +} + +$service = New-Object -ComObject Schedule.Service +try { + $service.Connect() +} +catch { + $module.FailJson("failed to connect to the task scheduler service: $($_.Exception.Message)", $_) +} + +try { + $task_folder = $service.GetFolder($path) + $module.Result.folder_exists = $true +} +catch { + $module.Result.folder_exists = $false + if ($null -ne $name) { + $module.Result.task_exists = $false + } + $module.ExitJson() +} + +$folder_tasks = $task_folder.GetTasks(1) +$folder_task_names = @() +$folder_task_count = 0 +$task = $null +for ($i = 1; $i -le $folder_tasks.Count; $i++) { + $task_name = $folder_tasks.Item($i).Name + $folder_task_names += $task_name + $folder_task_count += 1 + + if ($null -ne $name -and $task_name -eq $name) { + $task = $folder_tasks.Item($i) + } +} +$module.Result.folder_task_names = $folder_task_names +$module.Result.folder_task_count = $folder_task_count + +if ($null -ne $name) { + if ($null -ne $task) { + $module.Result.task_exists = $true + + # task state + $module.Result.state = @{ + last_run_time = (Get-Date $task.LastRunTime -Format s) + last_task_result = $task.LastTaskResult + next_run_time = (Get-Date $task.NextRunTime -Format s) + number_of_missed_runs = $task.NumberOfMissedRuns + status = [Enum]::ToObject([TASK_STATE], $task.State).ToString() + } + + # task definition + $task_definition = $task.Definition + $ignored_properties = @("XmlText") + $properties = @("principal", "registration_info", "settings") + $collection_properties = @("actions", "triggers") + + foreach ($property in $properties) { + $property_name = $property -replace "_" + $module.Result.$property = @{} + $values = $task_definition.$property_name + Get-Member -InputObject $values -MemberType Property | ForEach-Object { + if ($_.Name -notin $ignored_properties) { + $module.Result.$property.$($_.Name) = (Get-PropertyValue -task_property $property -com $values -property $_.Name) + } + } + } + + foreach ($property in $collection_properties) { + $module.Result.$property = @() + $collection = $task_definition.$property + $collection_count = $collection.Count + for ($i = 1; $i -le $collection_count; $i++) { + $item = $collection.Item($i) + $item_info = @{} + + Get-Member -InputObject $item -MemberType Property | ForEach-Object { + if ($_.Name -notin $ignored_properties) { + $value = (Get-PropertyValue -task_property $property -com $item -property $_.Name) + $item_info.$($_.Name) = $value + + # This was added after StateChange was represented by the raw enum value so we include both + # for backwards compatibility. + if ($_.Name -eq 'StateChange') { + $item_info.StateChangeStr = if ($value) { + [Enum]::ToObject([TASK_SESSION_STATE_CHANGE_TYPE], $value).ToString() + } + } + } + } + $module.Result.$property += $item_info + } + } + } + else { + $module.Result.task_exists = $false + } +} + +# Convert-DictToSnakeCase returns a Hashtable but Ansible.Basic expect a Dictionary. This is a hack until the snake +# conversion code has been moved to this collection and updated to handle this. +$new_result = [System.Collections.Generic.Dictionary[[String], [Object]]]@{} +foreach ($kvp in (Convert-DictToSnakeCase -dict $module.Result).GetEnumerator()) { + $new_result[$kvp.Name] = $kvp.Value +} +$module.Result = $new_result + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_scheduled_task_stat.py b/ansible_collections/community/windows/plugins/modules/win_scheduled_task_stat.py new file mode 100644 index 000000000..f19229677 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_scheduled_task_stat.py @@ -0,0 +1,371 @@ +#!/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) + +DOCUMENTATION = r''' +--- +module: win_scheduled_task_stat +short_description: Get information about Windows Scheduled Tasks +description: +- Will return whether the folder and task exists. +- Returns the names of tasks in the folder specified. +- Use M(community.windows.win_scheduled_task) to configure a scheduled task. +options: + path: + description: The folder path where the task lives. + type: str + default: \ + name: + description: + - The name of the scheduled task to get information for. + - If C(name) is set and exists, will return information on the task itself. + type: str +seealso: +- module: community.windows.win_scheduled_task +author: +- Jordan Borean (@jborean93) +''' + +EXAMPLES = r''' +- name: Get information about a folder + community.windows.win_scheduled_task_stat: + path: \folder name + register: task_folder_stat + +- name: Get information about a task in the root folder + community.windows.win_scheduled_task_stat: + name: task name + register: task_stat + +- name: Get information about a task in a custom folder + community.windows.win_scheduled_task_stat: + path: \folder name + name: task name + register: task_stat +''' + +RETURN = r''' +actions: + description: A list of actions. + returned: name is specified and task exists + type: list + sample: [ + { + "Arguments": "/c echo hi", + "Id": null, + "Path": "cmd.exe", + "Type": "TASK_ACTION_EXEC", + "WorkingDirectory": null + } + ] +folder_exists: + description: Whether the folder set at path exists. + returned: always + type: bool + sample: true +folder_task_count: + description: The number of tasks that exist in the folder. + returned: always + type: int + sample: 2 +folder_task_names: + description: A list of tasks that exist in the folder. + returned: always + type: list + sample: [ 'Task 1', 'Task 2' ] +principal: + description: Details on the principal configured to run the task. + returned: name is specified and task exists + type: complex + contains: + display_name: + description: The name of the user/group that is displayed in the Task + Scheduler UI. + returned: '' + type: str + sample: Administrator + group_id: + description: The group that will run the task. + returned: '' + type: str + sample: BUILTIN\Administrators + id: + description: The ID for the principal. + returned: '' + type: str + sample: Author + logon_type: + description: The logon method that the task will run with. + returned: '' + type: str + sample: TASK_LOGON_INTERACTIVE_TOKEN + run_level: + description: The level of user rights used to run the task. + returned: '' + type: str + sample: TASK_RUNLEVEL_LUA + user_id: + description: The user that will run the task. + returned: '' + type: str + sample: SERVER\Administrator +registration_info: + description: Details on the task registration info. + returned: name is specified and task exists + type: complex + contains: + author: + description: The author os the task. + returned: '' + type: str + sample: SERVER\Administrator + date: + description: The date when the task was register. + returned: '' + type: str + sample: '2017-01-01T10:00:00' + description: + description: The description of the task. + returned: '' + type: str + sample: task description + documentation: + description: The documentation of the task. + returned: '' + type: str + sample: task documentation + security_descriptor: + description: The security descriptor of the task. + returned: '' + type: str + sample: security descriptor + source: + description: The source of the task. + returned: '' + type: str + sample: source + uri: + description: The URI/path of the task. + returned: '' + type: str + sample: \task\task name + version: + description: The version of the task. + returned: '' + type: str + sample: 1.0 +settings: + description: Details on the task settings. + returned: name is specified and task exists + type: complex + contains: + allow_demand_start: + description: Whether the task can be started by using either the Run + command of the Context menu. + returned: '' + type: bool + sample: true + allow_hard_terminate: + description: Whether the task can terminated by using TerminateProcess. + returned: '' + type: bool + sample: true + compatibility: + description: The compatibility level of the task + returned: '' + type: int + sample: 2 + delete_expired_task_after: + description: The amount of time the Task Scheduler will wait before + deleting the task after it expires. + returned: '' + type: str + sample: PT10M + disallow_start_if_on_batteries: + description: Whether the task will not be started if the computer is + running on battery power. + returned: '' + type: bool + sample: false + disallow_start_on_remote_app_session: + description: Whether the task will not be started when in a remote app + session. + returned: '' + type: bool + sample: true + enabled: + description: Whether the task is enabled. + returned: '' + type: bool + sample: true + execution_time_limit: + description: The amount of time allowed to complete the task. + returned: '' + type: str + sample: PT72H + hidden: + description: Whether the task is hidden in the UI. + returned: '' + type: bool + sample: false + idle_settings: + description: The idle settings of the task. + returned: '' + type: dict + sample: { + "idle_duration": "PT10M", + "restart_on_idle": false, + "stop_on_idle_end": true, + "wait_timeout": "PT1H" + } + maintenance_settings: + description: The maintenance settings of the task. + returned: '' + type: str + sample: null + mulitple_instances: + description: Indicates the behaviour when starting a task that is already + running. + returned: '' + type: int + sample: 2 + network_settings: + description: The network settings of the task. + returned: '' + type: dict + sample: { + "id": null, + "name": null + } + priority: + description: The priority level of the task. + returned: '' + type: int + sample: 7 + restart_count: + description: The number of times that the task will attempt to restart + on failures. + returned: '' + type: int + sample: 0 + restart_interval: + description: How long the Task Scheduler will attempt to restart the + task. + returned: '' + type: str + sample: PT15M + run_only_id_idle: + description: Whether the task will run if the computer is in an idle + state. + returned: '' + type: bool + sample: true + run_only_if_network_available: + description: Whether the task will run only when a network is available. + returned: '' + type: bool + sample: false + start_when_available: + description: Whether the task can start at any time after its scheduled + time has passed. + returned: '' + type: bool + sample: false + stop_if_going_on_batteries: + description: Whether the task will be stopped if the computer begins to + run on battery power. + returned: '' + type: bool + sample: true + use_unified_scheduling_engine: + description: Whether the task will use the unified scheduling engine. + returned: '' + type: bool + sample: false + volatile: + description: Whether the task is volatile. + returned: '' + type: bool + sample: false + wake_to_run: + description: Whether the task will wake the computer when it is time to + run the task. + returned: '' + type: bool + sample: false +state: + description: Details on the state of the task + returned: name is specified and task exists + type: complex + contains: + last_run_time: + description: The time the registered task was last run. + returned: '' + type: str + sample: '2017-09-20T20:50:00' + last_task_result: + description: The results that were returned the last time the task was + run. + returned: '' + type: int + sample: 267009 + next_run_time: + description: The time when the task is next scheduled to run. + returned: '' + type: str + sample: '2017-09-20T22:50:00' + number_of_missed_runs: + description: The number of times a task has missed a scheduled run. + returned: '' + type: int + sample: 1 + status: + description: The status of the task, whether it is running, stopped, etc. + returned: '' + type: str + sample: TASK_STATE_RUNNING +task_exists: + description: Whether the task at the folder exists. + returned: name is specified + type: bool + sample: true +triggers: + description: A list of triggers. + returned: name is specified and task exists + type: list + sample: [ + { + "delay": "PT15M", + "enabled": true, + "end_boundary": null, + "execution_time_limit": null, + "id": null, + "repetition": { + "duration": null, + "interval": null, + "stop_at_duration_end": false + }, + "start_boundary": null, + "type": "TASK_TRIGGER_BOOT" + }, + { + "days_of_month": "5,15,30", + "enabled": true, + "end_boundary": null, + "execution_time_limit": null, + "id": null, + "months_of_year": "june,december", + "random_delay": null, + "repetition": { + "duration": null, + "interval": null, + "stop_at_duration_end": false + }, + "run_on_last_day_of_month": true, + "start_boundary": "2017-09-20T03:44:38", + "type": "TASK_TRIGGER_MONTHLY" + } + ] +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_scoop.ps1 b/ansible_collections/community/windows/plugins/modules/win_scoop.ps1 new file mode 100644 index 000000000..e8383c82c --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_scoop.ps1 @@ -0,0 +1,285 @@ +#!powershell + +# Copyright: (c) 2020, Jamie Magee <jamie.magee@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#Requires -Module Ansible.ModuleUtils.ArgvParser +#Requires -Module Ansible.ModuleUtils.CommandUtil +#AnsibleRequires -CSharpUtil Ansible.Basic + +$spec = @{ + options = @{ + architecture = @{ type = "str"; choices = "32bit", "64bit"; aliases = @(, "arch") } + independent = @{ type = "bool"; default = $false } + global = @{ type = "bool"; default = $false } + name = @{ type = "list"; elements = "str"; required = $true } + no_cache = @{ type = "bool"; default = $false } + purge = @{ type = "bool"; default = $false } + skip_checksum = @{ type = "bool"; default = $false } + state = @{ type = "str"; default = "present"; choices = "present", "absent" } + } + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$architecture = $module.Params.architecture +$independent = $module.Params.independent +$global = $module.Params.global +$name = $module.Params.name +$no_cache = $module.Params.no_cache +$purge = $module.Params.purge +$skip_checksum = $module.Params.skip_checksum +$state = $module.Params.state + +$module.Result.rc = 0 + +function Install-Scoop { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingInvokeExpression', '', Justification = 'This is one use case where we want to use iex')] + [CmdletBinding()] + param () + + # Scoop doesn't have refreshenv like Chocolatey + # Let's try to update PATH first + $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User") + + $scoop_app = Get-Command -Name scoop.ps1 -Type ExternalScript -ErrorAction SilentlyContinue + if ($module.CheckMode -and $null -eq $scoop_app) { + $module.Result.skipped = $true + $module.Result.msg = "Skipped check mode run on win_scoop as scoop.ps1 cannot be found on the system" + $module.ExitJson() + } + elseif ($null -eq $scoop_app) { + # We need to install scoop + # We run this in a separate process to make it easier to get the result in a failure and capture any output that + # might be sent to the host. We also need to enable TLS 1.2 in that process and not here so it can download the + # install script and other components. + $install_script = { + # Enable TLS1.2 if it's available but disabled (eg. .NET 4.5) + $security_protocols = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::SystemDefault + if ([Net.SecurityProtocolType].GetMember("Tls12").Count -gt 0) { + $security_protocols = $security_protcols -bor [Net.SecurityProtocolType]::Tls12 + } + [Net.ServicePointManager]::SecurityProtocol = $security_protocols + + $script = (New-Object System.Net.WebClient).DownloadString('https://get.scoop.sh') + $installer = [ScriptBlock]::Create($script) + $params = @{} + + $current_user = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() + if ($current_user.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + $params.RunAsAdmin = $true + } + . $installer -RunAsAdmin + } + + $enc_command = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($install_script.ToString())) + $cmd = "powershell.exe -NoProfile -NoLogo -EncodedCommand $enc_command" + + # Scoops installer does weird things and the exit code will most likely be 0. Use the presence of the scoop + # command as the indicator as to whether it was installed or not. + $res = Run-Command -Command $cmd -environment $environment + + # Refresh PATH + $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User") + + # locate the newly installed scoop.ps1 + $scoop_app = Get-Command -Name scoop.ps1 -Type ExternalScript -ErrorAction SilentlyContinue + + if ($null -eq $scoop_app -or -not (Test-Path -LiteralPath $scoop_app.Path)) { + $module.Result.rc = $res.rc + $module.Result.stdout = $res.stdout + $module.Result.stderr = $res.stderr + $module.FailJson("Failed to bootstrap scoop.ps1") + } + + $module.Warn("Scoop was missing from this system, so it was installed during this task run.") + $module.Result.changed = $true + } + + return $scoop_app.Path +} + +function Get-ScoopPackage { + param( + [Parameter(Mandatory = $true)] [string]$scoop_path + ) + + $command = Argv-ToString -arguments @("powershell.exe", $scoop_path, "export") + $res = Run-Command -Command $command + if ($res.rc -ne 0) { + $module.Result.command = $command + $module.Result.rc = $res.rc + $module.Result.stdout = $res.stdout + $module.Result.stderr = $res.stderr + $module.FailJson("Error checking installed packages") + } + + # Scoop since 0.2.3 output as JSON, older versions use a custom format + # https://github.com/ScoopInstaller/Scoop/commit/c4d1b9c22f943a810bed7f9f74d7d4d5c42d9a74 + try { + $exportObj = ConvertFrom-Json -InputObject $res.stdout -ErrorAction Stop + } + catch { + $res.stdout -split "`n" | + Select-String '(.*?) \(v:(.*?)\)( \*global\*)? \[(.*?)\](\{32bit\})?' | + ForEach-Object { + [PSCustomObject]@{ + Package = $_.Matches[0].Groups[1].Value + Version = $_.Matches[0].Groups[2].Value + Global = -not ([string]::IsNullOrWhiteSpace($_.Matches[0].Groups[3].Value)) + Bucket = $_.Matches[0].Groups[4].Value + x86 = -not ([string]::IsNullOrWhiteSpace($_.Matches[0].Groups[5].Value)) + } + } + return + } + + $exportObj.apps | ForEach-Object { + $options = @($_.Info -split ',' | ForEach-Object Trim | Where-Object { $_ }) + if ('Install failed' -in $options) { + return + } + + [PSCustomObject]@{ + Package = $_.Name + Version = $_.Version + Global = 'Global install' -in $options + Bucket = $_.Source + x86 = '32bit' -in $options + } + } +} + +function Get-InstallScoopPackageArgument { + $arguments = [System.Collections.Generic.List[String]]@() + + if ($architecture) { + $arguments.Add("--arch") + $arguments.Add($architecture) + } + if ($global) { + $arguments.Add("--global") + } + if ($independent) { + $arguments.Add("--independent") + } + if ($no_cache) { + $arguments.Add("--no-cache") + } + if ($skip_checksum) { + $arguments.Add("--skip") + } + + return , $arguments +} + +function Install-ScoopPackage { + param( + [Parameter(Mandatory = $true)] [string]$scoop_path, + [Parameter(Mandatory = $true)] [String[]]$packages + ) + $arguments = [System.Collections.Generic.List[String]]@("powershell.exe", $scoop_path, "install") + $arguments.AddRange($packages) + + $common_args = Get-InstallScoopPackageArgument + $arguments.AddRange($common_args) + + $command = Argv-ToString -arguments $arguments + + if (-not $module.CheckMode) { + $res = Run-Command -Command $command + if ($res.rc -ne 0) { + $module.Result.command = $command + $module.Result.rc = $res.rc + $module.Result.stdout = $res.stdout + $module.Result.stderr = $res.stderr + $module.FailJson("Error installing $packages") + } + + if ($module.Verbosity -gt 1) { + $module.Result.stdout = $res.stdout + } + } + $module.Result.changed = $true +} + +function Get-UninstallScoopPackageArgument { + $arguments = [System.Collections.Generic.List[String]]@() + + if ($global) { + $arguments.Add("--global") + } + if ($purge) { + $arguments.Add("--purge") + } + + return , $arguments +} + +function Uninstall-ScoopPackage { + param( + [Parameter(Mandatory = $true)] [string]$scoop_path, + [Parameter(Mandatory = $true)] [String[]]$packages + ) + + $arguments = [System.Collections.Generic.List[String]]@("powershell.exe", $scoop_path, "uninstall") + $arguments.AddRange($packages) + + $common_args = Get-UninstallScoopPackageArgument + $arguments.AddRange($common_args) + + $command = Argv-ToString -arguments $arguments + + if (-not $module.CheckMode) { + $res = Run-Command -Command $command + if ($res.rc -ne 0) { + $module.Result.command = $command + $module.Result.rc = $res.rc + $module.Result.stdout = $res.stdout + $module.Result.stderr = $res.stderr + $module.FailJson("Error uninstalling $packages") + } + + if ($module.Verbosity -gt 1) { + $module.Result.stdout = $res.stdout + } + if (-not ($res.stdout -match "ERROR '(.*?)' isn't installed.")) { + $module.Result.changed = $true + } + } + else { + $module.Result.changed = $true + } +} + +$scoop_path = Install-Scoop + +$installed_packages = @(Get-ScoopPackage -scoop_path $scoop_path) + +if ($state -in @("absent")) { + # Always attempt uninstall + # Packages can be in a broken state where they don't in appear scoop export + Uninstall-ScoopPackage -scoop_path $scoop_path -packages $name +} + +if ($state -in @("present")) { + $missing_packages = foreach ($package in $name) { + if ( + ($installed_packages.Package -notcontains $package) -or + (($installed_packages.Package -contains $package) -and ( + ((($installed_packages | Where-Object { $_.Package -eq $package }).Global -contains $true) -and -not $global) -or + ((($installed_packages | Where-Object { $_.Package -eq $package }).Global -notcontains $true) -and $global) + ) + ) + ) { + $package + } + } +} + +if ($missing_packages) { + Install-ScoopPackage -scoop_path $scoop_path -packages $missing_packages +} + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_scoop.py b/ansible_collections/community/windows/plugins/modules/win_scoop.py new file mode 100644 index 000000000..288e75e22 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_scoop.py @@ -0,0 +1,79 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Jamie Magee <jamie.magee@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_scoop +short_description: Manage packages using Scoop +description: +- Manage packages using Scoop. +- If Scoop is missing from the system, the module will install it. +options: + architecture: + description: + - Force Scoop to install the package of a specific process architecture. + type: str + choices: [ 32bit, 64bit ] + aliases: [ arch ] + global: + description: + - Install the app globally + type: bool + default: no + independent: + description: + - Don't install dependencies automatically + type: bool + default: no + name: + description: + - Name of the package(s) to be installed. + type: list + elements: str + required: yes + no_cache: + description: + - Don't use the download cache + type: bool + default: no + purge: + description: + - Remove all persistent data + type: bool + default: no + skip_checksum: + description: + - Skip hash validation + type: bool + default: no + state: + description: + - State of the package on the system. + - When C(absent), will ensure the package is not installed. + - When C(present), will ensure the package is installed. + type: str + choices: [ absent, present ] + default: present +seealso: +- module: chocolatey.chocolatey.win_chocolatey +- name: Scoop website + description: More information about Scoop + link: https://scoop.sh +- name: Scoop installer repository + description: GitHub repository for the Scoop installer + link: https://github.com/lukesampson/scoop +- name: Scoop main bucket + description: GitHub repository for the main bucket + link: https://github.com/ScoopInstaller/Main +author: +- Jamie Magee (@JamieMagee) +''' + +EXAMPLES = r''' +- name: Install jq. + community.windows.win_scoop: + name: jq +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_scoop_bucket.ps1 b/ansible_collections/community/windows/plugins/modules/win_scoop_bucket.ps1 new file mode 100644 index 000000000..70c02cb00 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_scoop_bucket.ps1 @@ -0,0 +1,125 @@ +#!powershell + +# Copyright: (c) 2020, Jamie Magee <jamie.magee@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic + +$spec = @{ + options = @{ + name = @{ type = "str"; required = $true } + repo = @{ type = "str" } + state = @{ type = "str"; default = "present"; choices = "present", "absent" } + } + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$name = $module.Params.name +$repo = $module.Params.repo +$state = $module.Params.state + +# Kept for backwards compatibility +$module.Result.rc = 0 + +function Get-Scoop { + # Scoop doesn't have refreshenv like Chocolatey + # Let's try to update PATH first + $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User") + + $scoop_app = Get-Command -Name scoop.ps1 -Type ExternalScript -ErrorAction SilentlyContinue + + if ($module.CheckMode -and $null -eq $scoop_app) { + $module.Result.skipped = $true + $module.Result.msg = "Skipped check mode run on win_scoop_bucket as scoop.ps1 cannot be found on the system" + $module.ExitJson() + } + + if ($null -eq $scoop_app -or -not (Test-Path -LiteralPath $scoop_app.Path)) { + $module.FailJson("Failed to find scoop.ps1, make sure it is added to the PATH") + } + + return $scoop_app.Path +} + +function Get-ScoopBucket { + param( + [Parameter(Mandatory = $true)] [string]$scoop_path + ) + + &$scoop_path bucket list +} + +function Uninstall-ScoopBucket { + param( + [Parameter(Mandatory = $true)] [string]$scoop_path, + [Parameter(Mandatory = $true)] [String]$bucket + ) + + $arguments = @( + "bucket", + "rm" + $bucket + if ($repo) { $repo } + ) + if (-not $module.CheckMode) { + $res = (&$scoop_path @arguments) -join "`n" + if (-not $?) { + $module.Result.stdout = $res + $module.Result.rc = 1 + $module.FailJson("Failed to remove scoop bucket, see stdout for more details") + } + elseif ($module.Verbosity -gt 1) { + $module.Result.stdout = $res + } + } + + $module.Result.changed = $true +} + +function Install-ScoopBucket { + param( + [Parameter(Mandatory = $true)] [string]$scoop_path, + [Parameter(Mandatory = $true)] [String]$bucket, + [String]$repo + ) + + $arguments = @( + "bucket" + "add" + $bucket + if ($repo) { $repo } + ) + if (-not $module.CheckMode) { + $res = (&$scoop_path @arguments) -join "`n" + if (-not $?) { + $module.Result.stdout = $res + $module.Result.rc = 1 + $module.FailJson("Failed to add scoop bucket, see stdout for more details") + } + elseif ($module.Verbosity -gt 1) { + $module.Result.stdout = $res + } + } + + $module.Result.changed = $true +} + +$scoop_path = Get-Scoop + +$installed_buckets = Get-ScoopBucket -scoop_path $scoop_path + +if ($state -in @("absent")) { + if ($installed_buckets.Name -contains $name) { + Uninstall-ScoopBucket -scoop_path $scoop_path -bucket $name + } +} + +if ($state -in @("present")) { + if ($installed_buckets.Name -notcontains $name) { + Install-ScoopBucket -scoop_path $scoop_path -bucket $name -repo $repo + } +} + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_scoop_bucket.py b/ansible_collections/community/windows/plugins/modules/win_scoop_bucket.py new file mode 100644 index 000000000..57bb94642 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_scoop_bucket.py @@ -0,0 +1,73 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Jamie Magee <jamie.magee@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_scoop_bucket +version_added: 1.0.0 +short_description: Manage Scoop buckets +description: +- Manage Scoop buckets +requirements: +- git +options: + name: + description: + - Name of the Scoop bucket. + type: str + required: yes + repo: + description: + - Git repository that contains the scoop bucket + type: str + state: + description: + - State of the Scoop bucket. + - When C(absent), will ensure the package is not installed. + - When C(present), will ensure the package is installed. + type: str + choices: [ absent, present ] + default: present +seealso: +- module: community.windows.win_scoop +- name: Scoop website + description: More information about Scoop + link: https://scoop.sh +- name: Scoop directory + description: A directory of buckets for the scoop package manager for Windows + link: https://rasa.github.io/scoop-directory/ +author: +- Jamie Magee (@JamieMagee) +''' + +EXAMPLES = r''' +- name: Add the extras bucket + community.windows.win_scoop_bucket: + name: extras + +- name: Remove the versions bucket + community.windows.win_scoop_bucket: + name: versions + state: absent + +- name: Add a custom bucket + community.windows.win_scoop_bucket: + name: my-bucket + repo: https://github.com/example/my-bucket +''' + +RETURN = r''' +rc: + description: The result code of the scoop action + returned: always + type: int + sample: 0 +stdout: + description: The raw output from the scoop action + returned: on failure or when verbosity is greater than 1 + type: str + sample: The test bucket was added successfully. +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_security_policy.ps1 b/ansible_collections/community/windows/plugins/modules/win_security_policy.ps1 new file mode 100644 index 000000000..dd7016fe2 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_security_policy.ps1 @@ -0,0 +1,224 @@ +#!powershell + +# Copyright: (c) 2017, Jordan Borean <jborean93@gmail.com> +# 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 Invoke-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 -LiteralPath $log_path + Remove-Item -LiteralPath $log_path -Force + + $return = @{ + log = ($log -join "`n").Trim() + stdout = $stdout + stderr = $stderr + rc = $LASTEXITCODE + } + + return $return +} + +Function Export-SecEdit() { + # GetTempFileName() will create a file but it doesn't have any content. This is problematic as secedit uses the + # encoding of the file at /cfg if it exists and because there is no BOM it will export using the "ANSI" encoding. + # By making sure the file exists and has a UTF-16-LE BOM we can be sure our parser reads the bytes as a string + # correctly. + $secedit_ini_path = [IO.Path]::GetTempFileName() + Set-Content -LiteralPath $secedit_ini_path -Value '' -Encoding Unicode + + # 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 = Invoke-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 -LiteralPath $secedit_ini_path).Length -eq 0)) { + Remove-Item -LiteralPath $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 -LiteralPath $secedit_db_path -Force # needs to be deleted for SecEdit.exe /import to work + + $ini_contents = ConvertTo-Ini -ini $ini + + # Use Unicode (UTF-16-LE) as that is the same across all PowerShell versions and we don't have to worry about + # changing ANSI encodings. + Set-Content -LiteralPath $secedit_ini_path -Value $ini_contents -Encoding Unicode + $result.changed = $true + + $import_result = Invoke-SecEdit -arguments @("/configure", "/db", $secedit_db_path, "/cfg", $secedit_ini_path, "/quiet") + $result.import_log = $import_result.log + Remove-Item -LiteralPath $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)" + } + + # https://github.com/ansible-collections/community.windows/issues/153 + # The LegalNoticeText entry is stored in the ini with type 7 (REG_MULTI_SZ) where each comma entry is read as a + # newline. When secedit imports the value it sets LegalNoticeText in the registry to be a REG_SZ type with the + # newlines but it also adds the extra null char at the end that REG_MULTI_SZ uses to denote the end of an entry. + # We manually trim off that extra null char so the legal text does not contain the unknown character symbol. + $legalPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' + $legalName = 'LegalNoticeText' + $prop = Get-ItemProperty -LiteralPath $legalPath + if ($legalName -in $prop.PSObject.Properties.Name) { + $existingText = $prop.LegalNoticeText.TrimEnd("`0") + Set-ItemProperty -LiteralPath $legalPath -Name $legalName -Value $existingText + } +} + +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 ansible.windows.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/ansible_collections/community/windows/plugins/modules/win_security_policy.py b/ansible_collections/community/windows/plugins/modules/win_security_policy.py new file mode 100644 index 000000000..acc00f2d2 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_security_policy.py @@ -0,0 +1,118 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_security_policy +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(ansible.windows.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(ansible.windows.win_user_right) module instead. +seealso: +- module: ansible.windows.win_user_right +author: +- Jordan Borean (@jborean93) +''' + +EXAMPLES = r''' +- name: Change the guest account name + community.windows.win_security_policy: + section: System Access + key: NewGuestName + value: Guest Account + +- name: Set the maximum password age + community.windows.win_security_policy: + section: System Access + key: MaximumPasswordAge + value: 15 + +- name: Do not store passwords using reversible encryption + community.windows.win_security_policy: + section: System Access + key: ClearTextPassword + value: 0 + +- name: Enable system events + community.windows.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/ansible_collections/community/windows/plugins/modules/win_shortcut.ps1 b/ansible_collections/community/windows/plugins/modules/win_shortcut.ps1 new file mode 100644 index 000000000..1b0ab650c --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_shortcut.ps1 @@ -0,0 +1,381 @@ +#!powershell + +# Copyright: (c) 2016, Dag Wieers (@dagwieers) <dag@wieers.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# Based on: http://powershellblogger.com/2016/01/create-shortcuts-lnk-or-url-files-with-powershell/ + +#AnsibleRequires -CSharpUtil Ansible.Basic +#Requires -Module Ansible.ModuleUtils.AddType + +$spec = @{ + options = @{ + src = @{ type = 'str' } + dest = @{ type = 'path'; required = $true } + state = @{ type = 'str'; default = 'present'; choices = @( 'absent', 'present' ) } + arguments = @{ type = 'str'; aliases = @( 'args' ) } + directory = @{ type = 'path' } + hotkey = @{ type = 'str'; no_log = $false } + icon = @{ type = 'path' } + description = @{ type = 'str' } + windowstyle = @{ type = 'str'; choices = @( 'maximized', 'minimized', 'normal' ) } + run_as_admin = @{ type = 'bool'; default = $false } + } + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$src = $module.Params.src +$dest = $module.Params.dest +$state = $module.Params.state +$arguments = $module.Params.arguments # NOTE: Variable $args is a special variable +$directory = $module.Params.directory +$hotkey = $module.Params.hotkey +$icon = $module.Params.icon +$description = $module.Params.description +$windowstyle = $module.Params.windowstyle +$run_as_admin = $module.Params.run_as_admin + +# Expand environment variables on non-path types +if ($null -ne $src) { + $src = [System.Environment]::ExpandEnvironmentVariables($src) +} +if ($null -ne $arguments) { + $arguments = [System.Environment]::ExpandEnvironmentVariables($arguments) +} +if ($null -ne $description) { + $description = [System.Environment]::ExpandEnvironmentVariables($description) +} + +$module.Result.changed = $false +$module.Result.dest = $dest +$module.Result.state = $state + +# TODO: look at consolidating other COM actions into the C# class for future compatibility +Add-CSharpType -AnsibleModule $module -References @' +using System; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using System.Text; + +namespace Ansible.Shortcut +{ + [ComImport()] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("000214F9-0000-0000-C000-000000000046")] + internal interface IShellLinkW + { + // We only care about GetPath and GetIDList, omit the other methods for now + void GetPath(StringBuilder pszFile, int cch, IntPtr pfd, UInt32 fFlags); + void GetIDList(out IntPtr ppidl); + } + + [ComImport()] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("45E2b4AE-B1C3-11D0-B92F-00A0C90312E1")] + internal interface IShellLinkDataList + { + void AddDataBlock(IntPtr pDataBlock); + void CopyDataBlock(uint dwSig, out IntPtr ppDataBlock); + void RemoveDataBlock(uint dwSig); + void GetFlags(out ShellLinkFlags dwFlags); + void SetFlags(ShellLinkFlags dwFlags); + } + + internal class NativeHelpers + { + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct SHFILEINFO + { + public IntPtr hIcon; + public int iIcon; + public UInt32 dwAttributes; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 260)] public char[] szDisplayName; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 80)] public char[] szTypeName; + } + } + + internal class NativeMethods + { + [DllImport("shell32.dll")] + public static extern void ILFree( + IntPtr pidl); + + [DllImport("shell32.dll")] + public static extern IntPtr SHGetFileInfoW( + IntPtr pszPath, + UInt32 dwFileAttributes, + ref NativeHelpers.SHFILEINFO psfi, + int sbFileInfo, + UInt32 uFlags); + + [DllImport("shell32.dll")] + public static extern int SHParseDisplayName( + [MarshalAs(UnmanagedType.LPWStr)] string pszName, + IntPtr pbc, + out IntPtr ppidl, + UInt32 sfagoIn, + out UInt32 psfgaoOut); + } + + [System.Flags] + public enum ShellLinkFlags : uint + { + Default = 0x00000000, + HasIdList = 0x00000001, + HasLinkInfo = 0x00000002, + HasName = 0x00000004, + HasRelPath = 0x00000008, + HasWorkingDir = 0x00000010, + HasArgs = 0x00000020, + HasIconLocation = 0x00000040, + Unicode = 0x00000080, + ForceNoLinkInfo = 0x00000100, + HasExpSz = 0x00000200, + RunInSeparate = 0x00000400, + HasLogo3Id = 0x00000800, + HasDarwinId = 0x00001000, + RunAsUser = 0x00002000, + HasExpIconSz = 0x00004000, + NoPidlAlias = 0x00008000, + ForceUncName = 0x00010000, + RunWithShimLayer = 0x00020000, + ForceNoLinkTrack = 0x00040000, + EnableTargetMetadata = 0x00080000, + DisableLinkPathTracking = 0x00100000, + DisableKnownFolderRelativeTracking = 0x00200000, + NoKfAlias = 0x00400000, + AllowLinkToLink = 0x00800000, + UnAliasOnSave = 0x01000000, + PreferEnvironmentPath = 0x02000000, + KeepLocalIdListForUncTarget = 0x04000000, + PersistVolumeIdToRelative = 0x08000000, + Valid = 0x0FFFF7FF, + Reserved = 0x80000000 + } + + public class ShellLink + { + private static Guid CLSID_ShellLink = new Guid("00021401-0000-0000-C000-000000000046"); + + public static ShellLinkFlags GetFlags(string path) + { + IShellLinkW link = InitialiseObj(path); + ShellLinkFlags dwFlags; + ((IShellLinkDataList)link).GetFlags(out dwFlags); + return dwFlags; + } + + public static void SetFlags(string path, ShellLinkFlags flags) + { + IShellLinkW link = InitialiseObj(path); + ((IShellLinkDataList)link).SetFlags(flags); + ((IPersistFile)link).Save(null, false); + } + + public static string GetTargetPath(string path) + { + IShellLinkW link = InitialiseObj(path); + + StringBuilder pathSb = new StringBuilder(260); + link.GetPath(pathSb, pathSb.Capacity, IntPtr.Zero, 0); + string linkPath = pathSb.ToString(); + + // If the path wasn't set, try and get the path from the ItemIDList + ShellLinkFlags flags = GetFlags(path); + if (String.IsNullOrEmpty(linkPath) && ((uint)flags & (uint)ShellLinkFlags.HasIdList) == (uint)ShellLinkFlags.HasIdList) + { + IntPtr idList = IntPtr.Zero; + try + { + link.GetIDList(out idList); + linkPath = GetDisplayNameFromPidl(idList); + } + finally + { + NativeMethods.ILFree(idList); + } + } + return linkPath; + } + + public static string GetDisplayNameFromPath(string path) + { + UInt32 sfgaoOut; + IntPtr pidl = IntPtr.Zero; + try + { + int res = NativeMethods.SHParseDisplayName(path, IntPtr.Zero, out pidl, 0, out sfgaoOut); + Marshal.ThrowExceptionForHR(res); + return GetDisplayNameFromPidl(pidl); + } + finally + { + NativeMethods.ILFree(pidl); + } + } + + private static string GetDisplayNameFromPidl(IntPtr pidl) + { + NativeHelpers.SHFILEINFO shFileInfo = new NativeHelpers.SHFILEINFO(); + UInt32 uFlags = 0x000000208; // SHGFI_DISPLAYNAME | SHGFI_PIDL + NativeMethods.SHGetFileInfoW(pidl, 0, ref shFileInfo, Marshal.SizeOf(typeof(NativeHelpers.SHFILEINFO)), uFlags); + return new string(shFileInfo.szDisplayName).TrimEnd('\0'); + } + + private static IShellLinkW InitialiseObj(string path) + { + IShellLinkW link = Activator.CreateInstance(Type.GetTypeFromCLSID(CLSID_ShellLink)) as IShellLinkW; + ((IPersistFile)link).Load(path, 0); + return link; + } + } +} +'@ + +# Convert from window style name to window style id +$windowstyles = @{ + normal = 1 + maximized = 3 + minimized = 7 +} + +# Convert from window style id to window style name +$windowstyleids = @( "", "normal", "", "maximized", "", "", "", "minimized" ) + +If ($state -eq "absent") { + If (Test-Path -LiteralPath $dest) { + # If the shortcut exists, try to remove it + Try { + Remove-Item -LiteralPath $dest -WhatIf:$module.CheckMode + } + Catch { + # Report removal failure + $module.FailJson("Failed to remove shortcut '$dest'. ($($_.Exception.Message))", $_) + } + # Report removal success + $module.Result.changed = $true + } + Else { + # Nothing to report, everything is fine already + } +} +ElseIf ($state -eq "present") { + # Create an in-memory object based on the existing shortcut (if any) + $Shell = New-Object -ComObject ("WScript.Shell") + $ShortCut = $Shell.CreateShortcut($dest) + + # Compare existing values with new values, report as changed if required + + If ($null -ne $src) { + # Windows translates executables to absolute path, so do we + If (Get-Command -Name $src -Type Application -ErrorAction SilentlyContinue) { + $src = (Get-Command -Name $src -Type Application).Definition + } + If (-not (Test-Path -LiteralPath $src -IsValid)) { + If (-not (Split-Path -Path $src -IsAbsolute)) { + $module.FailJson("Source '$src' is not found in PATH and not a valid or absolute path.") + } + } + } + + # Determine if we have a WshShortcut or WshUrlShortcut by checking the Arguments property + # A WshUrlShortcut objects only consists of a TargetPath property + + $file_shortcut = $false + If (Get-Member -InputObject $ShortCut -Name Arguments) { + # File ShortCut, compare multiple properties + $file_shortcut = $true + + $target_path = $ShortCut.TargetPath + If (($null -ne $src) -and ($ShortCut.TargetPath -ne $src)) { + if ((Test-Path -LiteralPath $dest) -and (-not $ShortCut.TargetPath)) { + # If the shortcut already exists but not on the COM object, we + # are dealing with a shell path like 'shell:RecycleBinFolder'. + $expanded_src = [Ansible.Shortcut.ShellLink]::GetDisplayNameFromPath($src) + $actual_src = [Ansible.Shortcut.ShellLink]::GetTargetPath($dest) + if ($expanded_src -ne $actual_src) { + $module.Result.changed = $true + $ShortCut.TargetPath = $src + } + } + else { + $module.Result.changed = $true + $ShortCut.TargetPath = $src + } + $target_path = $src + } + + # This is a full-featured application shortcut ! + If (($null -ne $arguments) -and ($ShortCut.Arguments -ne $arguments)) { + $module.Result.changed = $true + $ShortCut.Arguments = $arguments + } + $module.Result.args = $ShortCut.Arguments + + If (($null -ne $directory) -and ($ShortCut.WorkingDirectory -ne $directory)) { + $module.Result.changed = $true + $ShortCut.WorkingDirectory = $directory + } + $module.Result.directory = $ShortCut.WorkingDirectory + + # FIXME: Not all values are accepted here ! Improve docs too. + If (($null -ne $hotkey) -and ($ShortCut.Hotkey -ne $hotkey)) { + $module.Result.changed = $true + $ShortCut.Hotkey = $hotkey + } + $module.Result.hotkey = $ShortCut.Hotkey + + If (($null -ne $icon) -and ($ShortCut.IconLocation -ne $icon)) { + $module.Result.changed = $true + $ShortCut.IconLocation = $icon + } + $module.Result.icon = $ShortCut.IconLocation + + If (($null -ne $description) -and ($ShortCut.Description -ne $description)) { + $module.Result.changed = $true + $ShortCut.Description = $description + } + $module.Result.description = $ShortCut.Description + + If (($null -ne $windowstyle) -and ($ShortCut.WindowStyle -ne $windowstyles.$windowstyle)) { + $module.Result.changed = $true + $ShortCut.WindowStyle = $windowstyles.$windowstyle + } + $module.Result.windowstyle = $windowstyleids[$ShortCut.WindowStyle] + } + else { + # URL Shortcut, just compare the TargetPath + if (($null -ne $src) -and ($ShortCut.TargetPath -ne $src)) { + $module.Result.changed = $true + $ShortCut.TargetPath = $src + } + $target_path = $ShortCut.TargetPath + } + $module.Result.src = $target_path + + If (($module.Result.changed -eq $true) -and ($module.CheckMode -ne $true)) { + Try { + $ShortCut.Save() + } + Catch { + $module.FailJson("Failed to create shortcut '$dest'. ($($_.Exception.Message))", $_) + } + } + + if ((Test-Path -LiteralPath $dest) -and $file_shortcut) { + # Only control the run_as_admin flag if using a File Shortcut + $flags = [Ansible.Shortcut.ShellLink]::GetFlags($dest) + if ($run_as_admin -and (-not $flags.HasFlag([Ansible.Shortcut.ShellLinkFlags]::RunAsUser))) { + [Ansible.Shortcut.ShellLink]::SetFlags($dest, ($flags -bor [Ansible.Shortcut.ShellLinkFlags]::RunAsUser)) + $module.Result.changed = $true + } + elseif (-not $run_as_admin -and ($flags.HasFlag([Ansible.Shortcut.ShellLinkFlags]::RunAsUser))) { + [Ansible.Shortcut.ShellLink]::SetFlags($dest, ($flags -bxor [Ansible.Shortcut.ShellLinkFlags]::RunAsUser)) + $module.Result.changed = $true + } + } +} + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_shortcut.py b/ansible_collections/community/windows/plugins/modules/win_shortcut.py new file mode 100644 index 000000000..8fcc55fd1 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_shortcut.py @@ -0,0 +1,116 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2016, Dag Wieers (@dagwieers) <dag@wieers.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_shortcut +short_description: Manage shortcuts on Windows +description: +- Create, manage and delete Windows shortcuts +options: + src: + description: + - Executable or URL the shortcut points to. + - The executable needs to be in your PATH, or has to be an absolute + path to the executable. + type: str + description: + description: + - Description for the shortcut. + - This is usually shown when hoovering the icon. + type: str + dest: + description: + - Destination file for the shortcuting file. + - File name should have a C(.lnk) or C(.url) extension. + type: path + required: yes + arguments: + description: + - Additional arguments for the executable defined in C(src). + type: str + aliases: [ args ] + directory: + description: + - Working directory for executable defined in C(src). + type: path + icon: + description: + - Icon used for the shortcut. + - File name should have a C(.ico) extension. + - The file name is followed by a comma and the number in the library file (.dll) or use 0 for an image file. + type: path + hotkey: + description: + - Key combination for the shortcut. + - This is a combination of one or more modifiers and a key. + - Possible modifiers are Alt, Ctrl, Shift, Ext. + - Possible keys are [A-Z] and [0-9]. + type: str + windowstyle: + description: + - Influences how the application is displayed when it is launched. + type: str + choices: [ maximized, minimized, normal ] + state: + description: + - When C(absent), removes the shortcut if it exists. + - When C(present), creates or updates the shortcut. + type: str + choices: [ absent, present ] + default: present + run_as_admin: + description: + - When C(src) is an executable, this can control whether the shortcut will be opened as an administrator or not. + type: bool + default: no +notes: +- 'The following options can include Windows environment variables: C(dest), C(args), C(description), C(dest), C(directory), C(icon) C(src)' +- 'Windows has two types of shortcuts: Application and URL shortcuts. URL shortcuts only consists of C(dest) and C(src)' +seealso: +- module: ansible.windows.win_file +author: +- Dag Wieers (@dagwieers) +''' + +EXAMPLES = r''' +- name: Create an application shortcut on the desktop + community.windows.win_shortcut: + src: C:\Program Files\Mozilla Firefox\Firefox.exe + dest: C:\Users\Public\Desktop\Mozilla Firefox.lnk + icon: C:\Program Files\Mozilla Firefox\Firefox.exe,0 + +- name: Create the same shortcut using environment variables + community.windows.win_shortcut: + description: The Mozilla Firefox web browser + src: '%ProgramFiles%\Mozilla Firefox\Firefox.exe' + dest: '%Public%\Desktop\Mozilla Firefox.lnk' + icon: '%ProgramFiles\Mozilla Firefox\Firefox.exe,0' + directory: '%ProgramFiles%\Mozilla Firefox' + hotkey: Ctrl+Alt+F + +- name: Create an application shortcut for an executable in PATH to your desktop + community.windows.win_shortcut: + src: cmd.exe + dest: Desktop\Command prompt.lnk + +- name: Create an application shortcut for the Ansible website + community.windows.win_shortcut: + src: '%ProgramFiles%\Google\Chrome\Application\chrome.exe' + dest: '%UserProfile%\Desktop\Ansible website.lnk' + arguments: --new-window https://ansible.com/ + directory: '%ProgramFiles%\Google\Chrome\Application' + icon: '%ProgramFiles%\Google\Chrome\Application\chrome.exe,0' + hotkey: Ctrl+Alt+A + +- name: Create a URL shortcut for the Ansible website + community.windows.win_shortcut: + src: https://ansible.com/ + dest: '%Public%\Desktop\Ansible website.url' +''' + +RETURN = r''' +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_snmp.ps1 b/ansible_collections/community/windows/plugins/modules/win_snmp.ps1 new file mode 100644 index 000000000..7e4b9af2a --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_snmp.ps1 @@ -0,0 +1,132 @@ +#!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 + +$params = Parse-Args -arguments $args -supports_check_mode $true +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false +$managers = Get-AnsibleParam -obj $params -name "permitted_managers" -type "list" -default $null +$communities = Get-AnsibleParam -obj $params -name "community_strings" -type "list" -default $null +$action_in = Get-AnsibleParam -obj $params -name "action" -type "str" -default "set" -ValidateSet @("set", "add", "remove") +$action = $action_in.ToLower() + +$result = @{ + failed = $False + changed = $False + community_strings = [System.Collections.ArrayList]@() + permitted_managers = [System.Collections.ArrayList]@() +} + +# Make sure lists are modifyable +[System.Collections.ArrayList]$managers = $managers +[System.Collections.ArrayList]$communities = $communities +[System.Collections.ArrayList]$indexes = @() + +# Type checking +# You would think that "$null -ne $managers" would work, but it doesn't. +# A proper type check is required. If a user provides an empty list then $managers +# is still of the correct type. If a user provides no option then $managers is $null. +If ($managers -Is [System.Collections.ArrayList] -And $managers.Count -gt 0 -And $managers[0] -IsNot [String]) { + Fail-Json $result "Permitted managers must be an array of strings" +} + +If ($communities -Is [System.Collections.ArrayList] -And $communities.Count -gt 0 -And $communities[0] -IsNot [String]) { + Fail-Json $result "SNMP communities must be an array of strings" +} + +$Managers_reg_key = "HKLM:\System\CurrentControlSet\services\SNMP\Parameters\PermittedManagers" +$Communities_reg_key = "HKLM:\System\CurrentControlSet\services\SNMP\Parameters\ValidCommunities" + +ForEach ($idx in (Get-Item -LiteralPath $Managers_reg_key).Property) { + $manager = (Get-ItemProperty -LiteralPath $Managers_reg_key).$idx + If ($idx.ToLower() -eq '(default)') { + continue + } + + $remove = $False + If ($managers -Is [System.Collections.ArrayList] -And $managers.Contains($manager)) { + If ($action -eq "remove") { + $remove = $True + } + Else { + # Remove manager from list to add since it already exists + $managers.Remove($manager) + } + } + ElseIf ($action -eq "set" -And $managers -Is [System.Collections.ArrayList]) { + # Will remove this manager since it is not in the set list + $remove = $True + } + + If ($remove) { + $result.changed = $True + Remove-ItemProperty -LiteralPath $Managers_reg_key -Name $idx -WhatIf:$check_mode + } + Else { + # Remember that this index is in use + $indexes.Add([int]$idx) | Out-Null + $result.permitted_managers.Add($manager) | Out-Null + } +} + +ForEach ($community in (Get-Item -LiteralPath $Communities_reg_key).Property) { + If ($community.ToLower() -eq '(default)') { + continue + } + + $remove = $False + If ($communities -Is [System.Collections.ArrayList] -And $communities.Contains($community)) { + If ($action -eq "remove") { + $remove = $True + } + Else { + # Remove community from list to add since it already exists + $communities.Remove($community) + } + } + ElseIf ($action -eq "set" -And $communities -Is [System.Collections.ArrayList]) { + # Will remove this community since it is not in the set list + $remove = $True + } + + If ($remove) { + $result.changed = $True + Remove-ItemProperty -LiteralPath $Communities_reg_key -Name $community -WhatIf:$check_mode + } + Else { + $result.community_strings.Add($community) | Out-Null + } +} + +If ($action -eq "remove") { + Exit-Json $result +} + +# Add managers that don't already exist +$next_index = 0 +If ($managers -Is [System.Collections.ArrayList]) { + ForEach ($manager in $managers) { + While ($True) { + $next_index = $next_index + 1 + If (-Not $indexes.Contains($next_index)) { + $result.changed = $True + New-ItemProperty -LiteralPath $Managers_reg_key -Name $next_index -Value "$manager" -WhatIf:$check_mode | Out-Null + $result.permitted_managers.Add($manager) | Out-Null + break + } + } + } +} + +# Add communities that don't already exist +If ($communities -Is [System.Collections.ArrayList]) { + ForEach ($community in $communities) { + $result.changed = $True + New-ItemProperty -LiteralPath $Communities_reg_key -Name $community -PropertyType DWord -Value 4 -WhatIf:$check_mode | Out-Null + $result.community_strings.Add($community) | Out-Null + } +} + +Exit-Json $result diff --git a/ansible_collections/community/windows/plugins/modules/win_snmp.py b/ansible_collections/community/windows/plugins/modules/win_snmp.py new file mode 100644 index 000000000..0068e2144 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_snmp.py @@ -0,0 +1,70 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Ansible, inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_snmp +short_description: Configures the Windows SNMP service +description: + - This module configures the Windows SNMP service. +options: + permitted_managers: + description: + - The list of permitted SNMP managers. + type: list + elements: str + community_strings: + description: + - The list of read-only SNMP community strings. + type: list + elements: str + action: + description: + - C(add) will add new SNMP community strings and/or SNMP managers + - C(set) will replace SNMP community strings and/or SNMP managers. An + empty list for either C(community_strings) or C(permitted_managers) + will result in the respective lists being removed entirely. + - C(remove) will remove SNMP community strings and/or SNMP managers + type: str + choices: [ add, set, remove ] + default: set +author: + - Michael Cassaniti (@mcassaniti) +''' + +EXAMPLES = r''' +- name: Replace SNMP communities and managers + community.windows.win_snmp: + community_strings: + - public + permitted_managers: + - 192.168.1.2 + action: set + +- name: Replace SNMP communities and clear managers + community.windows.win_snmp: + community_strings: + - public + permitted_managers: [] + action: set +''' + +RETURN = r''' +community_strings: + description: The list of community strings for this machine. + type: list + returned: always + sample: + - public + - snmp-ro +permitted_managers: + description: The list of permitted managers for this machine. + type: list + returned: always + sample: + - 192.168.1.1 + - 192.168.1.2 +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_timezone.ps1 b/ansible_collections/community/windows/plugins/modules/win_timezone.ps1 new file mode 100644 index 000000000..c2f3b4a96 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_timezone.ps1 @@ -0,0 +1,73 @@ +#!powershell + +# Copyright: (c) 2015, Phil Schwartz <schwartzmx@gmail.com> +# 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 +$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 + +$timezone = Get-AnsibleParam -obj $params -name "timezone" -type "str" -failifempty $true + +$result = @{ + changed = $false + previous_timezone = $timezone + timezone = $timezone +} + +Try { + # Get the current timezone set + $result.previous_timezone = $(tzutil.exe /g) + If ($LASTEXITCODE -ne 0) { + Throw "An error occurred when getting the current machine's timezone setting." + } + + if ( $result.previous_timezone -eq $timezone ) { + Exit-Json $result "Timezone '$timezone' is already set on this machine" + } + Else { + # Check that timezone is listed as an available timezone to the machine + $tzList = $(tzutil.exe /l).ToLower() + If ($LASTEXITCODE -ne 0) { + Throw "An error occurred when listing the available timezones." + } + + $tzExists = $tzList.Contains(($timezone -Replace '_dstoff').ToLower()) + if (-not $tzExists) { + Fail-Json $result "The specified timezone: $timezone isn't supported on the machine." + } + + if ($check_mode) { + $result.changed = $true + } + else { + tzutil.exe /s "$timezone" + if ($LASTEXITCODE -ne 0) { + Throw "An error occurred when setting the specified timezone with tzutil." + } + + $new_timezone = $(tzutil.exe /g) + if ($LASTEXITCODE -ne 0) { + Throw "An error occurred when getting the current machine's timezone setting." + } + + if ($timezone -eq $new_timezone) { + $result.changed = $true + } + } + + if ($diff_support) { + $result.diff = @{ + before = "$($result.previous_timezone)`n" + after = "$timezone`n" + } + } + } +} +Catch { + Fail-Json $result "Error setting timezone to: $timezone." +} + +Exit-Json $result diff --git a/ansible_collections/community/windows/plugins/modules/win_timezone.py b/ansible_collections/community/windows/plugins/modules/win_timezone.py new file mode 100644 index 000000000..d7f6adba8 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_timezone.py @@ -0,0 +1,63 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2015, Phil Schwartz <schwartzmx@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_timezone +short_description: Sets Windows machine timezone +description: +- Sets machine time to the specified timezone. +options: + timezone: + description: + - Timezone to set to. + - 'Example: Central Standard Time' + - To disable Daylight Saving time, add the suffix C(_dstoff) on timezones that support this. + type: str + required: yes +notes: +- The module will check if the provided timezone is supported on the machine. +- A list of possible timezones is available from C(tzutil.exe /l) and from + U(https://msdn.microsoft.com/en-us/library/ms912391.aspx) +- If running on Server 2008 the hotfix + U(https://support.microsoft.com/en-us/help/2556308/tzutil-command-line-tool-is-added-to-windows-vista-and-to-windows-server-2008) + needs to be installed to be able to run this module. +seealso: +- module: community.windows.win_region +author: +- Phil Schwartz (@schwartzmx) +''' + +EXAMPLES = r''' +- name: Set timezone to 'Romance Standard Time' (GMT+01:00) + community.windows.win_timezone: + timezone: Romance Standard Time + +- name: Set timezone to 'GMT Standard Time' (GMT) + community.windows.win_timezone: + timezone: GMT Standard Time + +- name: Set timezone to 'Central Standard Time' (GMT-06:00) + community.windows.win_timezone: + timezone: Central Standard Time + +- name: Set timezime to Pacific Standard time and disable Daylight Saving time adjustments + community.windows.win_timezone: + timezone: Pacific Standard Time_dstoff +''' + +RETURN = r''' +previous_timezone: + description: The previous timezone if it was changed, otherwise the existing timezone. + returned: success + type: str + sample: Central Standard Time +timezone: + description: The current timezone (possibly changed). + returned: success + type: str + sample: Central Standard Time +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_toast.ps1 b/ansible_collections/community/windows/plugins/modules/win_toast.ps1 new file mode 100644 index 000000000..5cb1cf26f --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_toast.ps1 @@ -0,0 +1,93 @@ +#!powershell + +# Copyright: (c) 2017, Jon Hawkesworth (@jhawkesworth) <jhawkesworth@protonmail.com> +# 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" + +# version check +$osversion = [Environment]::OSVersion +$lowest_version = 10 +if ($osversion.Version.Major -lt $lowest_version ) { + $msg = "Sorry, this version of windows, $osversion, does not support Toast notifications. Toast notifications are available from version $lowest_version" + Fail-Json -obj $result -message $msg +} + +$stopwatch = [system.diagnostics.stopwatch]::startNew() +$now = [DateTime]::Now +$default_title = "Notification: " + $now.ToShortTimeString() + +$params = Parse-Args $args -supports_check_mode $true +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false + +$expire_seconds = Get-AnsibleParam -obj $params -name "expire" -type "int" -default 45 +$group = Get-AnsibleParam -obj $params -name "group" -type "str" -default "Powershell" +$msg = Get-AnsibleParam -obj $params -name "msg" -type "str" -default "Hello world!" +$popup = Get-AnsibleParam -obj $params -name "popup" -type "bool" -default $true +$tag = Get-AnsibleParam -obj $params -name "tag" -type "str" -default "Ansible" +$title = Get-AnsibleParam -obj $params -name "title" -type "str" -default $default_title + +$timespan = New-TimeSpan -Seconds $expire_seconds +$expire_at = $now + $timespan +$expire_at_utc = $($expire_at.ToUniversalTime() | Out-String).Trim() + +$result = @{ + changed = $false + expire_at = $expire_at.ToString() + expire_at_utc = $expire_at_utc + toast_sent = $false +} + +# If no logged in users, there is no notifications service, +# and no-one to read the message, so exit but do not fail +# if there are no logged in users to notify. + +if ((Get-Process -Name explorer -ErrorAction SilentlyContinue).Count -gt 0) { + + [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null + $template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText01) + + #Convert to .NET type for XML manipulation + $toastXml = [xml] $template.GetXml() + $toastXml.GetElementsByTagName("text").AppendChild($toastXml.CreateTextNode($title)) > $null + # TODO add subtitle + + #Convert back to WinRT type + $xml = New-Object Windows.Data.Xml.Dom.XmlDocument + $xml.LoadXml($toastXml.OuterXml) + + $toast = [Windows.UI.Notifications.ToastNotification]::new($xml) + $toast.Tag = $tag + $toast.Group = $group + $toast.ExpirationTime = $expire_at + $toast.SuppressPopup = -not $popup + + try { + $notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($msg) + if (-not $check_mode) { + $notifier.Show($toast) + $result.toast_sent = $true + Start-Sleep -Seconds $expire_seconds + } + } + catch { + $excep = $_ + $result.exception = $excep.ScriptStackTrace + Fail-Json -obj $result -message "Failed to create toast notifier: $($excep.Exception.Message)" + } +} +else { + $result.toast_sent = $false + $result.no_toast_sent_reason = 'No logged in users to notify' +} + +$endsend_at = Get-Date | Out-String +$stopwatch.Stop() + +$result.time_taken = $stopwatch.Elapsed.TotalSeconds +$result.sent_localtime = $endsend_at.Trim() + +Exit-Json -obj $result diff --git a/ansible_collections/community/windows/plugins/modules/win_toast.py b/ansible_collections/community/windows/plugins/modules/win_toast.py new file mode 100644 index 000000000..e3fc1a079 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_toast.py @@ -0,0 +1,94 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2017, Jon Hawkesworth (@jhawkesworth) <jhawkesworth@protonmail.com> +# Copyright: (c) 2017, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_toast +short_description: Sends Toast windows notification to logged in users on Windows 10 or later hosts +description: + - Sends alerts which appear in the Action Center area of the windows desktop. +options: + expire: + description: + - How long in seconds before the notification expires. + type: int + default: 45 + group: + description: + - Which notification group to add the notification to. + type: str + default: Powershell + msg: + description: + - The message to appear inside the notification. + - May include \n to format the message to appear within the Action Center. + type: str + default: Hello, World! + popup: + description: + - If C(no), the notification will not pop up and will only appear in the Action Center. + type: bool + default: yes + tag: + description: + - The tag to add to the notification. + type: str + default: Ansible + title: + description: + - The notification title, which appears in the pop up.. + type: str + default: Notification HH:mm +notes: + - This module must run on a windows 10 or Server 2016 host, so ensure your play targets windows hosts, or delegates to a windows host. + - The module does not fail if there are no logged in users to notify. + - Messages are only sent to the local host where the module is run. + - You must run this module with async, otherwise it will hang until the expire period has passed. +seealso: +- module: community.windows.win_msg +- module: community.windows.win_say +author: +- Jon Hawkesworth (@jhawkesworth) +''' + +EXAMPLES = r''' +- name: Warn logged in users of impending upgrade (note use of async to stop the module from waiting until notification expires). + community.windows.win_toast: + expire: 60 + title: System Upgrade Notification + msg: Automated upgrade about to start. Please save your work and log off before {{ deployment_start_time }} + async: 60 + poll: 0 +''' + +RETURN = r''' +expire_at_utc: + description: Calculated utc date time when the notification expires. + returned: always + type: str + sample: 07 July 2017 04:50:54 +no_toast_sent_reason: + description: Text containing the reason why a notification was not sent. + returned: when no logged in users are detected + type: str + sample: No logged in users to notify +sent_localtime: + description: local date time when the notification was sent. + returned: always + type: str + sample: 07 July 2017 05:45:54 +time_taken: + description: How long the module took to run on the remote windows host in seconds. + returned: always + type: float + sample: 0.3706631999999997 +toast_sent: + description: Whether the module was able to send a toast notification or not. + returned: always + type: bool + sample: false +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_unzip.ps1 b/ansible_collections/community/windows/plugins/modules/win_unzip.ps1 new file mode 100644 index 000000000..bd2133e2b --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_unzip.ps1 @@ -0,0 +1,197 @@ +#!powershell + +# Copyright: (c) 2015, Phil Schwartz <schwartzmx@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#Requires -Module Ansible.ModuleUtils.Legacy + +# TODO: This module is not idempotent (it will always unzip and report change) + +$ErrorActionPreference = "Stop" + +$pcx_extensions = @('.bz2', '.gz', '.msu', '.tar', '.zip') + +$params = Parse-Args $args -supports_check_mode $true +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false + +$src = Get-AnsibleParam -obj $params -name "src" -type "path" -failifempty $true +$dest = Get-AnsibleParam -obj $params -name "dest" -type "path" -failifempty $true +$creates = Get-AnsibleParam -obj $params -name "creates" -type "path" +$recurse = Get-AnsibleParam -obj $params -name "recurse" -type "bool" -default $false +$delete_archive = Get-AnsibleParam -obj $params -name "delete_archive" -type "bool" -default $false -aliases 'rm' +$password = Get-AnsibleParam -obj $params -name "password" -type "str" + +# Fixes a fail error message (when the task actually succeeds) for a +# "Convert-ToJson: The converted JSON string is in bad format" +# This happens when JSON is parsing a string that ends with a "\", +# which is possible when specifying a directory to download to. +# This catches that possible error, before assigning the JSON $result +$result = @{ + changed = $false + dest = $dest -replace '\$', '' + removed = $false + src = $src -replace '\$', '' +} + +Function Expand-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 + + $entry_target_path = [System.IO.Path]::Combine($dest, $archive_name) + $entry_dir = [System.IO.Path]::GetDirectoryName($entry_target_path) + + # Normalize paths for further evaluation + $full_target_path = [System.IO.Path]::GetFullPath($entry_target_path) + $full_dest_path = [System.IO.Path]::GetFullPath($dest + [System.IO.Path]::DirectorySeparatorChar) + + # Ensure file in the archive does not escape the extraction path + if (-not $full_target_path.StartsWith($full_dest_path)) { + $msg = "Error unzipping '$src' to '$dest'! Filename contains relative paths which would extract outside the destination: $entry_target_path" + Fail-Json -obj $result -message $msg + } + + if (-not (Test-Path -LiteralPath $entry_dir)) { + New-Item -Path $entry_dir -ItemType Directory -WhatIf:$check_mode | Out-Null + $result.changed = $true + } + + if ((-not ($entry_target_path.EndsWith("\") -or $entry_target_path.EndsWith("/"))) -and (-not $check_mode)) { + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $entry_target_path, $true) + } + $result.changed = $true + } + $archive.Dispose() +} + +Function Expand-ZipLegacy($src, $dest) { + # [System.IO.Compression.ZipFile] was only added in .net 4.5, this is used + # when .net is older than that. + $shell = New-Object -ComObject Shell.Application + $zip = $shell.NameSpace([IO.Path]::GetFullPath($src)) + $dest_path = $shell.NameSpace([IO.Path]::GetFullPath($dest)) + + $shell = New-Object -ComObject Shell.Application + + if (-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($zip.Items(), 1044) + } + $result.changed = $true +} + +If ($creates -and (Test-Path -LiteralPath $creates)) { + $result.skipped = $true + $result.msg = "The file or directory '$creates' already exists." + Exit-Json -obj $result +} + +If (-Not (Test-Path -LiteralPath $src)) { + Fail-Json -obj $result -message "File '$src' does not exist." +} + +$ext = [System.IO.Path]::GetExtension($src) + +If (-Not (Test-Path -LiteralPath $dest -PathType Container)) { + Try { + New-Item -ItemType "directory" -path $dest -WhatIf:$check_mode | out-null + } + Catch { + Fail-Json -obj $result -message "Error creating '$dest' directory! Msg: $($_.Exception.Message)" + } +} + +If ($ext -eq ".zip" -And $recurse -eq $false -And -Not $password) { + # TODO: PS v5 supports zip extraction, use that if available + $use_legacy = $false + try { + # determines if .net 4.5 is available, if this fails we need to fall + # back to the legacy COM Shell.Application to extract the zip + Add-Type -AssemblyName System.IO.Compression.FileSystem | Out-Null + Add-Type -AssemblyName System.IO.Compression | Out-Null + } + catch { + $use_legacy = $true + } + + if ($use_legacy) { + try { + Expand-ZipLegacy -src $src -dest $dest + } + catch { + Fail-Json -obj $result -message "Error unzipping '$src' to '$dest'!. Method: COM Shell.Application, Exception: $($_.Exception.Message)" + } + } + else { + try { + Expand-Zip -src $src -dest $dest + } + catch { + Fail-Json -obj $result -message "Error unzipping '$src' to '$dest'!. Method: System.IO.Compression.ZipFile, Exception: $($_.Exception.Message)" + } + } +} +Else { + # Check if PSCX is installed + $list = Get-Module -ListAvailable + + If (-Not ($list -match "PSCX")) { + Fail-Json -obj $result -message "PowerShellCommunityExtensions PowerShell Module (PSCX) is required for non-'.zip' compressed archive types." + } + Else { + $result.pscx_status = "present" + } + + Try { + Import-Module PSCX + } + Catch { + Fail-Json $result "Error importing module PSCX" + } + + $expand_params = @{ + OutputPath = $dest + WhatIf = $check_mode + } + if ($null -ne $password) { + $expand_params.Password = ConvertTo-SecureString -String $password -AsPlainText -Force + } + Try { + Expand-Archive -Path $src @expand_params + } + Catch { + Fail-Json -obj $result -message "Error expanding '$src' to '$dest'! Msg: $($_.Exception.Message)" + } + + If ($recurse) { + Get-ChildItem -LiteralPath $dest -recurse | Where-Object { $pcx_extensions -contains $_.extension } | ForEach-Object { + Try { + Expand-Archive -Path $_.FullName -Force @expand_params + } + Catch { + Fail-Json -obj $result -message "Error recursively expanding '$src' to '$dest'! Msg: $($_.Exception.Message)" + } + If ($delete_archive) { + Remove-Item -LiteralPath $_.FullName -Force -WhatIf:$check_mode + $result.removed = $true + } + } + } + + $result.changed = $true +} + +If ($delete_archive) { + try { + Remove-Item -LiteralPath $src -Recurse -Force -WhatIf:$check_mode + } + catch { + Fail-Json -obj $result -message "failed to delete archive at '$src': $($_.Exception.Message)" + } + $result.removed = $true +} +Exit-Json $result diff --git a/ansible_collections/community/windows/plugins/modules/win_unzip.py b/ansible_collections/community/windows/plugins/modules/win_unzip.py new file mode 100644 index 000000000..f5c46c80a --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_unzip.py @@ -0,0 +1,112 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2015, Phil Schwartz <schwartzmx@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_unzip +short_description: Unzips compressed files and archives on the Windows node +description: +- Unzips compressed files and archives. +- Supports .zip files natively. +- Supports other formats supported by the Powershell Community Extensions (PSCX) module (basically everything 7zip supports). +- For non-Windows targets, use the M(ansible.builtin.unarchive) module instead. +requirements: +- PSCX +options: + src: + description: + - File to be unzipped (provide absolute path). + type: path + required: yes + dest: + description: + - Destination of zip file (provide absolute path of directory). If it does not exist, the directory will be created. + type: path + required: yes + delete_archive: + description: + - Remove the zip file, after unzipping. + type: bool + default: no + aliases: [ rm ] + recurse: + description: + - Recursively expand zipped files within the src file. + - Setting to a value of C(yes) requires the PSCX module to be installed. + type: bool + default: no + creates: + description: + - If this file or directory exists the specified src will not be extracted. + type: path + password: + description: + - If a zip file is encrypted with password. + - Passing a value to a password parameter requires the PSCX module to be installed. +notes: +- This module is not really idempotent, it will extract the archive every time, and report a change. +- For extracting any compression types other than .zip, the PowerShellCommunityExtensions (PSCX) Module is required. This module (in conjunction with PSCX) + has the ability to recursively unzip files within the src zip file provided and also functionality for many other compression types. If the destination + directory does not exist, it will be created before unzipping the file. Specifying rm parameter will force removal of the src file after extraction. +seealso: +- module: ansible.builtin.unarchive +author: +- Phil Schwartz (@schwartzmx) +''' + +EXAMPLES = r''' +# This unzips a library that was downloaded with win_get_url, and removes the file after extraction +# $ ansible -i hosts -m win_unzip -a "src=C:\LibraryToUnzip.zip dest=C:\Lib remove=yes" all + +- name: Unzip a bz2 (BZip) file + community.windows.win_unzip: + src: C:\Users\Phil\Logs.bz2 + dest: C:\Users\Phil\OldLogs + creates: C:\Users\Phil\OldLogs + +- name: Unzip gz log + community.windows.win_unzip: + src: C:\Logs\application-error-logs.gz + dest: C:\ExtractedLogs\application-error-logs + +# Unzip .zip file, recursively decompresses the contained .gz files and removes all unneeded compressed files after completion. +- name: Recursively decompress GZ files in ApplicationLogs.zip + community.windows.win_unzip: + src: C:\Downloads\ApplicationLogs.zip + dest: C:\Application\Logs + recurse: yes + delete_archive: yes + +- name: Install PSCX + community.windows.win_psmodule: + name: Pscx + state: present + +- name: Unzip .7z file which is password encrypted + community.windows.win_unzip: + src: C:\Downloads\ApplicationLogs.7z + dest: C:\Application\Logs + password: abcd + delete_archive: yes +''' + +RETURN = r''' +dest: + description: The provided destination path + returned: always + type: str + sample: C:\ExtractedLogs\application-error-logs +removed: + description: Whether the module did remove any files during task run + returned: always + type: bool + sample: true +src: + description: The provided source path + returned: always + type: str + sample: C:\Logs\application-error-logs.gz +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_user_profile.ps1 b/ansible_collections/community/windows/plugins/modules/win_user_profile.ps1 new file mode 100644 index 000000000..babe627ca --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_user_profile.ps1 @@ -0,0 +1,169 @@ +#!powershell + +# Copyright: (c) 2019, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic + +$spec = @{ + options = @{ + name = @{ type = "str" } + remove_multiple = @{ type = "bool"; default = $false } + state = @{ type = "str"; default = "present"; choices = @("absent", "present") } + username = @{ type = "sid"; } + } + required_if = @( + @("state", "present", @("username")), + @("state", "absent", @("name", "username"), $true) + ) + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) +$module.Result.path = $null + +$name = $module.Params.name +$remove_multiple = $module.Params.remove_multiple +$state = $module.Params.state +$username = $module.Params.username + +Add-CSharpType -AnsibleModule $module -References @' +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ansible.WinUserProfile +{ + public class NativeMethods + { + [DllImport("Userenv.dll", CharSet = CharSet.Unicode)] + public static extern int CreateProfile( + [MarshalAs(UnmanagedType.LPWStr)] string pszUserSid, + [MarshalAs(UnmanagedType.LPWStr)] string pszUserName, + [Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszProfilePath, + UInt32 cchProfilePath); + + [DllImport("Userenv.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool DeleteProfileW( + [MarshalAs(UnmanagedType.LPWStr)] string lpSidString, + IntPtr lpProfile, + IntPtr lpComputerName); + + [DllImport("Userenv.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool GetProfilesDirectoryW( + [Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder lpProfileDir, + ref UInt32 lpcchSize); + } +} +'@ + +Function Get-LastWin32ExceptionMessage { + param([int]$ErrorCode) + $exp = New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList $ErrorCode + $exp_msg = "{0} (Win32 ErrorCode {1} - 0x{1:X8})" -f $exp.Message, $ErrorCode + return $exp_msg +} + +Function Get-ExpectedProfilePath { + param([String]$BaseName) + + # Environment.GetFolderPath does not have an enumeration to get the base profile dir, use PInvoke instead + # and combine with the base name to return back to the user - best efforts + $profile_path_length = 0 + [Ansible.WinUserProfile.NativeMethods]::GetProfilesDirectoryW($null, + [ref]$profile_path_length) > $null + + $raw_profile_path = New-Object -TypeName System.Text.StringBuilder -ArgumentList $profile_path_length + $res = [Ansible.WinUserProfile.NativeMethods]::GetProfilesDirectoryW($raw_profile_path, + [ref]$profile_path_length) + + if ($res -eq $false) { + $msg = Get-LastWin32ExceptionMessage -Error ([System.Runtime.InteropServices.Marshal]::GetLastWin32Error()) + $module.FailJson("Failed to determine profile path with the base name '$BaseName': $msg") + } + $profile_path = Join-Path -Path $raw_profile_path.ToString() -ChildPath $BaseName + + return $profile_path +} + +$profiles = Get-ChildItem -LiteralPath "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" + +if ($state -eq "absent") { + if ($null -ne $username) { + $user_profiles = $profiles | Where-Object { $_.PSChildName -eq $username.Value } + } + else { + # If the username was not provided, or we are removing a profile for a deleted user, we need to try and find + # the correct SID to delete. We just verify that the path matches based on the name passed in + $expected_profile_path = Get-ExpectedProfilePath -BaseName $name + + $user_profiles = $profiles | Where-Object { + $profile_path = (Get-ItemProperty -LiteralPath $_.PSPath -Name ProfileImagePath).ProfileImagePath + $profile_path -eq $expected_profile_path + } + + if ($user_profiles.Length -gt 1 -and -not $remove_multiple) { + $msg = "Found multiple profiles matching the path '$expected_profile_path', set 'remove_multiple=True' to remove all the profiles for this match" + $module.FailJson($msg) + } + } + + foreach ($user_profile in $user_profiles) { + $profile_path = (Get-ItemProperty -LiteralPath $user_profile.PSPath -Name ProfileImagePath).ProfileImagePath + if (-not $module.CheckMode) { + $res = [Ansible.WinUserProfile.NativeMethods]::DeleteProfileW($user_profile.PSChildName, [IntPtr]::Zero, + [IntPtr]::Zero) + if ($res -eq $false) { + $msg = Get-LastWin32ExceptionMessage -Error ([System.Runtime.InteropServices.Marshal]::GetLastWin32Error()) + $module.FailJson("Failed to delete the profile for $($user_profile.PSChildName): $msg") + } + } + + # While we may have multiple profiles when the name option was used, it will always be the same path due to + # how we match name to a profile so setting it mutliple time sis fine + $module.Result.path = $profile_path + $module.Result.changed = $true + } +} +elseif ($state -eq "present") { + # Now we know the SID, see if the profile already exists + $user_profile = $profiles | Where-Object { $_.PSChildName -eq $username.Value } + if ($null -eq $user_profile) { + # In case a SID was set as the username we still need to make sure the SID is mapped to a valid local account + try { + $account_name = $username.Translate([System.Security.Principal.NTAccount]) + } + catch [System.Security.Principal.IdentityNotMappedException] { + $module.FailJson("Fail to map the account '$($username.Value)' to a valid user") + } + + # If the basename was not provided, determine it from the actual username + if ($null -eq $name) { + $name = $account_name.Value.Split('\', 2)[-1] + } + + if ($module.CheckMode) { + $profile_path = Get-ExpectedProfilePath -BaseName $name + } + else { + $raw_profile_path = New-Object -TypeName System.Text.StringBuilder -ArgumentList 260 + $res = [Ansible.WinUserProfile.NativeMethods]::CreateProfile($username.Value, $name, $raw_profile_path, + $raw_profile_path.Capacity) + + if ($res -ne 0) { + $exp = [System.Runtime.InteropServices.Marshal]::GetExceptionForHR($res) + $module.FailJson("Failed to create profile for user '$username': $($exp.Message)") + } + $profile_path = $raw_profile_path.ToString() + } + + $module.Result.changed = $true + $module.Result.path = $profile_path + } + else { + $module.Result.path = (Get-ItemProperty -LiteralPath $user_profile.PSPath -Name ProfileImagePath).ProfileImagePath + } +} + +$module.ExitJson() + diff --git a/ansible_collections/community/windows/plugins/modules/win_user_profile.py b/ansible_collections/community/windows/plugins/modules/win_user_profile.py new file mode 100644 index 000000000..280de1559 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_user_profile.py @@ -0,0 +1,109 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_user_profile +short_description: Manages the Windows user profiles. +description: +- Used to create or remove user profiles on a Windows host. +- This can be used to create a profile before a user logs on or delete a + profile when removing a user account. +- A profile can be created for both a local or domain account. +options: + name: + description: + - Specifies the base name for the profile path. + - When I(state) is C(present) this is used to create the profile for + I(username) at a specific path within the profile directory. + - This cannot be used to specify a path outside of the profile directory + but rather it specifies a folder(s) within this directory. + - If a profile for another user already exists at the same path, then a 3 + digit incremental number is appended by Windows automatically. + - When I(state) is C(absent) and I(username) is not set, then the module + will remove all profiles that point to the profile path derived by this + value. + - This is useful if the account no longer exists but the profile still + remains. + type: str + remove_multiple: + description: + - When I(state) is C(absent) and the value for I(name) matches multiple + profiles the module will fail. + - Set this value to C(yes) to force the module to delete all the profiles + found. + default: no + type: bool + state: + description: + - Will ensure the profile exists when set to C(present). + - When creating a profile the I(username) option must be set to a valid + account. + - Will remove the profile(s) when set to C(absent). + - When removing a profile either I(username) must be set to a valid + account, or I(name) is set to the profile's base name. + default: present + choices: + - absent + - present + type: str + username: + description: + - The account name of security identifier (SID) for the profile. + - This must be set when I(state) is C(present) and must be a valid account + or the SID of a valid account. + - When I(state) is C(absent) then this must still be a valid account number + but the SID can be a deleted user's SID. + type: sid +seealso: +- module: ansible.windows.win_user +- module: community.windows.win_domain_user +author: +- Jordan Borean (@jborean93) +''' + +EXAMPLES = r''' +- name: Create a profile for an account + community.windows.win_user_profile: + username: ansible-account + state: present + +- name: Create a profile for an account at C:\Users\ansible + community.windows.win_user_profile: + username: ansible-account + name: ansible + state: present + +- name: Remove a profile for a still valid account + community.windows.win_user_profile: + username: ansible-account + state: absent + +- name: Remove a profile for a deleted account + community.windows.win_user_profile: + name: ansible + state: absent + +- name: Remove a profile for a deleted account based on the SID + community.windows.win_user_profile: + username: S-1-5-21-3233007181-2234767541-1895602582-1305 + state: absent + +- name: Remove multiple profiles that exist at the basename path + community.windows.win_user_profile: + name: ansible + state: absent + remove_multiple: yes +''' + +RETURN = r''' +path: + description: The full path to the profile for the account. This will be null + if C(state=absent) and no profile was deleted. + returned: always + type: str + sample: C:\Users\ansible +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_wait_for_process.ps1 b/ansible_collections/community/windows/plugins/modules/win_wait_for_process.ps1 new file mode 100644 index 000000000..fa9f5a635 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_wait_for_process.ps1 @@ -0,0 +1,176 @@ +#!powershell + +# Copyright: (c) 2017, Ansible Project +# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic +#Requires -Module Ansible.ModuleUtils.SID + +$spec = @{ + options = @{ + process_name_exact = @{ type = 'list'; elements = 'str' } + process_name_pattern = @{ type = 'str' } + pid = @{ type = 'int'; default = 0 } + owner = @{ type = 'str' } + sleep = @{ type = 'int'; default = 1 } + pre_wait_delay = @{ type = 'int'; default = 0 } + post_wait_delay = @{ type = 'int'; default = 0 } + process_min_count = @{ type = 'int'; default = 1 } + state = @{ type = 'str'; default = 'present'; choices = @( 'absent', 'present' ) } + timeout = @{ type = 'int'; default = 300 } + } + mutually_exclusive = @( + @( 'pid', 'process_name_exact' ), + @( 'pid', 'process_name_pattern' ), + @( 'process_name_exact', 'process_name_pattern' ) + ) + required_one_of = @( + , @( 'owner', 'pid', 'process_name_exact', 'process_name_pattern' ) + ) + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$process_name_exact = $module.Params.process_name_exact +$process_name_pattern = $module.Params.process_name_pattern +$process_id = $module.Params.pid # pid is a reserved variable in PowerShell, using process_id instead +$owner = $module.Params.owner +$sleep = $module.Params.sleep +$pre_wait_delay = $module.Params.pre_wait_delay +$post_wait_delay = $module.Params.post_wait_delay +$process_min_count = $module.Params.process_min_count +$state = $module.Params.state +$timeout = $module.Params.timeout + +$module.Result.changed = $false +$module.Result.elapsed = 0 +$module.Result.matched_processes = @() + +# Validate the input +if ($state -eq "absent" -and $sleep -ne 1) { + $module.Warn("Parameter 'sleep' has no effect when waiting for a process to stop.") +} + +if ($state -eq "absent" -and $process_min_count -ne 1) { + $module.Warn("Parameter 'process_min_count' has no effect when waiting for a process to stop.") +} + +if ($owner -and ("IncludeUserName" -notin (Get-Command -Name Get-Process).Parameters.Keys)) { + $module.FailJson("This version of Powershell does not support filtering processes by 'owner'.") +} + +Function Get-FilteredProcess { + [cmdletbinding()] + Param( + [String] + $Owner, + $ProcessNameExact, + $ProcessNamePattern, + [int] + $ProcessId + ) + + $FilteredProcesses = @() + + try { + $Processes = Get-Process -IncludeUserName + $SupportsUserNames = $true + } + catch [System.Management.Automation.ParameterBindingException] { + $Processes = Get-Process + $SupportsUserNames = $false + } + + foreach ($Process in $Processes) { + + # If a process name was specified in the filter, validate that here. + if ($ProcessNamePattern) { + if ($Process.ProcessName -notmatch $ProcessNamePattern) { + continue + } + } + + # If a process name was specified in the filter, validate that here. + if ($ProcessNameExact -and $ProcessNameExact -notcontains $Process.ProcessName) { + continue + } + + # If a PID was specified in the filter, validate that here. + if ($ProcessId -and $ProcessId -ne 0) { + if ($ProcessId -ne $Process.Id) { + continue + } + } + + # If an owner was specified in the filter, validate that here. + if ($Owner) { + if (-not $Process.UserName) { + continue + } + elseif ((Convert-ToSID($Owner)) -ne (Convert-ToSID($Process.UserName))) { + # NOTE: This is rather expensive + continue + } + } + + if ($SupportsUserNames -eq $true) { + $FilteredProcesses += @{ name = $Process.ProcessName; pid = $Process.Id; owner = $Process.UserName } + } + else { + $FilteredProcesses += @{ name = $Process.ProcessName; pid = $Process.Id } + } + } + + return , $FilteredProcesses +} + +$module_start = Get-Date +Start-Sleep -Seconds $pre_wait_delay + +if ($state -eq "present" ) { + + # Wait for a process to start + do { + + $Processes = Get-FilteredProcess -Owner $owner -ProcessNameExact $process_name_exact -ProcessNamePattern $process_name_pattern -ProcessId $process_id + $module.Result.matched_processes = $Processes + + if ($Processes.count -ge $process_min_count) { + break + } + + if (((Get-Date) - $module_start).TotalSeconds -gt $timeout) { + $module.Result.elapsed = ((Get-Date) - $module_start).TotalSeconds + $module.FailJson("Timed out while waiting for process(es) to start") + } + + Start-Sleep -Seconds $sleep + + } while ($true) + +} +elseif ($state -eq "absent") { + + # Wait for a process to stop + $Processes = Get-FilteredProcess -Owner $owner -ProcessNameExact $process_name_exact -ProcessNamePattern $process_name_pattern -ProcessId $process_id + $module.Result.matched_processes = $Processes + + if ($Processes.count -gt 0 ) { + try { + # This may randomly fail when used on specially protected processes (think: svchost) + Wait-Process -Id $Processes.pid -Timeout $timeout + } + catch [System.TimeoutException] { + $module.Result.elapsed = ((Get-Date) - $module_start).TotalSeconds + $module.FailJson("Timeout while waiting for process(es) to stop") + } + } + +} + +Start-Sleep -Seconds $post_wait_delay +$module.Result.elapsed = ((Get-Date) - $module_start).TotalSeconds + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_wait_for_process.py b/ansible_collections/community/windows/plugins/modules/win_wait_for_process.py new file mode 100644 index 000000000..6f483d10b --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_wait_for_process.py @@ -0,0 +1,128 @@ +#!/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) + +DOCUMENTATION = r''' +--- +module: win_wait_for_process +short_description: Waits for a process to exist or not exist before continuing. +description: +- Waiting for a process to start or stop. +- This is useful when Windows services behave poorly and do not enumerate external dependencies in their manifest. +options: + process_name_exact: + description: + - The name of the process(es) for which to wait. The name of the process(es) should not include the file extension suffix. + type: list + elements: str + process_name_pattern: + description: + - RegEx pattern matching desired process(es). + type: str + sleep: + description: + - Number of seconds to sleep between checks. + - Only applies when waiting for a process to start. Waiting for a process to start + does not have a native non-polling mechanism. Waiting for a stop uses native PowerShell + and does not require polling. + type: int + default: 1 + process_min_count: + description: + - Minimum number of process matching the supplied pattern to satisfy C(present) condition. + - Only applies to C(present). + type: int + default: 1 + pid: + description: + - The PID of the process. + default: 0 + type: int + owner: + description: + - The owner of the process. + - Requires PowerShell version 4.0 or newer. + type: str + pre_wait_delay: + description: + - Seconds to wait before checking processes. + type: int + default: 0 + post_wait_delay: + description: + - Seconds to wait after checking for processes. + type: int + default: 0 + state: + description: + - When checking for a running process C(present) will block execution + until the process exists, or until the timeout has been reached. + C(absent) will block execution until the process no longer exists, + or until the timeout has been reached. + - When waiting for C(present), the module will return changed only if + the process was not present on the initial check but became present on + subsequent checks. + - If, while waiting for C(absent), new processes matching the supplied + pattern are started, these new processes will not be included in the + action. + type: str + default: present + choices: [ absent, present ] + timeout: + description: + - The maximum number of seconds to wait for a for a process to start or stop + before erroring out. + type: int + default: 300 +seealso: +- module: ansible.builtin.wait_for +- module: ansible.windows.win_wait_for +author: +- Charles Crossan (@crossan007) +''' + +EXAMPLES = r''' +- name: Wait 300 seconds for all Oracle VirtualBox processes to stop. (VBoxHeadless, VirtualBox, VBoxSVC) + community.windows.win_wait_for_process: + process_name_pattern: 'v(irtual)?box(headless|svc)?' + state: absent + timeout: 500 + +- name: Wait 300 seconds for 3 instances of cmd to start, waiting 5 seconds between each check + community.windows.win_wait_for_process: + process_name_exact: cmd + state: present + timeout: 500 + sleep: 5 + process_min_count: 3 +''' + +RETURN = r''' +elapsed: + description: The elapsed seconds between the start of poll and the end of the module. + returned: always + type: float + sample: 3.14159265 +matched_processes: + description: List of matched processes (either stopped or started). + returned: always + type: complex + contains: + name: + description: The name of the matched process. + returned: always + type: str + sample: svchost + owner: + description: The owner of the matched process. + returned: when supported by PowerShell + type: str + sample: NT AUTHORITY\SYSTEM + pid: + description: The PID of the matched process. + returned: always + type: int + sample: 7908 +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_wakeonlan.ps1 b/ansible_collections/community/windows/plugins/modules/win_wakeonlan.ps1 new file mode 100644 index 000000000..ea4a4a5eb --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_wakeonlan.ps1 @@ -0,0 +1,52 @@ +#!powershell + +# Copyright: (c) 2017, Dag Wieers (@dagwieers) <dag@wieers.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic + +$spec = @{ + options = @{ + mac = @{ type = 'str'; required = $true } + broadcast = @{ type = 'str'; default = '255.255.255.255' } + port = @{ type = 'int'; default = 7 } + } + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$module.Result.changed = $false + +$mac = $module.Params.mac +$mac_orig = $module.Params.mac +$broadcast = $module.Params.broadcast +$port = $module.Params.port + +$broadcast = [Net.IPAddress]::Parse($broadcast) + +# Remove possible separator from MAC address +if ($mac.Length -eq (12 + 5)) { + $mac = $mac.Replace($mac.Substring(2, 1), "") +} + +# If we don't end up with 12 hexadecimal characters, fail +if ($mac.Length -ne 12) { + $module.FailJson("Incorrect MAC address: $mac_orig") +} + +# Create payload for magic packet +# TODO: Catch possible conversion errors +$target = 0, 2, 4, 6, 8, 10 | ForEach-Object { [convert]::ToByte($mac.Substring($_, 2), 16) } +$data = (, [byte]255 * 6) + ($target * 20) + +# Broadcast payload to network +$udpclient = new-Object System.Net.Sockets.UdpClient +if (-not $module.CheckMode) { + $udpclient.Connect($broadcast, $port) + [void] $udpclient.Send($data, 102) +} + +$module.Result.changed = $true + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_wakeonlan.py b/ansible_collections/community/windows/plugins/modules/win_wakeonlan.py new file mode 100644 index 000000000..b9ba920b9 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_wakeonlan.py @@ -0,0 +1,57 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2017, Dag Wieers <dag@wieers.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_wakeonlan +short_description: Send a magic Wake-on-LAN (WoL) broadcast packet +description: +- The C(win_wakeonlan) module sends magic Wake-on-LAN (WoL) broadcast packets. +- For non-Windows targets, use the M(community.general.wakeonlan) module instead. +options: + mac: + description: + - MAC address to send Wake-on-LAN broadcast packet for. + type: str + required: yes + broadcast: + description: + - Network broadcast address to use for broadcasting magic Wake-on-LAN packet. + type: str + default: 255.255.255.255 + port: + description: + - UDP port to use for magic Wake-on-LAN packet. + type: int + default: 7 +todo: +- Does not have SecureOn password support +notes: +- This module sends a magic packet, without knowing whether it worked. It always report a change. +- Only works if the target system was properly configured for Wake-on-LAN (in the BIOS and/or the OS). +- Some BIOSes have a different (configurable) Wake-on-LAN boot order (i.e. PXE first). +seealso: +- module: community.general.wakeonlan +author: +- Dag Wieers (@dagwieers) +''' + +EXAMPLES = r''' +- name: Send a magic Wake-on-LAN packet to 00:00:5E:00:53:66 + community.windows.win_wakeonlan: + mac: 00:00:5E:00:53:66 + broadcast: 192.0.2.23 + +- name: Send a magic Wake-On-LAN packet on port 9 to 00-00-5E-00-53-66 + community.windows.win_wakeonlan: + mac: 00-00-5E-00-53-66 + port: 9 + delegate_to: remote_system +''' + +RETURN = r''' +# Default return values +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_webpicmd.ps1 b/ansible_collections/community/windows/plugins/modules/win_webpicmd.ps1 new file mode 100644 index 000000000..f888d7d0a --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_webpicmd.ps1 @@ -0,0 +1,103 @@ +#!powershell + +# Copyright: (c) 2015, Peter Mounce <public@neverrunwithscissors.com> +# 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" + +Function Find-InstalledCommand { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, Position = 0)] [string] $command + ) + $installed = get-command $command -erroraction Ignore + write-verbose "$installed" + if ($installed) { + return $installed + } + return $null +} + +Function Find-WebPiCmd { + [CmdletBinding()] + param() + $p = Find-InstalledCommand "webpicmd.exe" + if ($null -ne $p) { + return $p + } + $a = Find-InstalledCommand "c:\programdata\chocolatey\bin\webpicmd.exe" + if ($null -ne $a) { + return $a + } + Throw "webpicmd.exe is not installed. It must be installed (use chocolatey)" +} + +Function Test-IsInstalledFromWebPI { + [CmdletBinding()] + + param( + [Parameter(Mandatory = $true, Position = 0)] + [string]$package + ) + + $results = &$executable /list /listoption:installed + + if ($LastExitCode -ne 0) { + $result.webpicmd_error_cmd = $cmd + $result.webpicmd_error_log = "$results" + + Throw "Error checking installation status for $package" + } + Write-Verbose "$results" + + if ($results -match "^$package\s+") { + return $true + } + + return $false +} + +Function Install-WithWebPICmd { + [CmdletBinding()] + + param( + [Parameter(Mandatory = $true, Position = 0)] + [string]$package + ) + + $results = &$executable /install /products:$package /accepteula /suppressreboot + + if ($LastExitCode -ne 0) { + $result.webpicmd_error_cmd = $cmd + $result.webpicmd_error_log = "$results" + Throw "Error installing $package" + } + + write-verbose "$results" + + if ($results -match "Install of Products: SUCCESS") { + $result.changed = $true + } +} + +$result = @{ + changed = $false +} + +$params = Parse-Args $args + +$package = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true + +Try { + $script:executable = Find-WebPiCmd + if ((Test-IsInstalledFromWebPI -package $package) -eq $false) { + Install-WithWebPICmd -package $package + } + + Exit-Json $result +} +Catch { + Fail-Json $result $_.Exception.Message +} diff --git a/ansible_collections/community/windows/plugins/modules/win_webpicmd.py b/ansible_collections/community/windows/plugins/modules/win_webpicmd.py new file mode 100644 index 000000000..16745fb07 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_webpicmd.py @@ -0,0 +1,35 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2015, Peter Mounce <public@neverrunwithscissors.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_webpicmd +short_description: Installs packages using Web Platform Installer command-line +description: + - Installs packages using Web Platform Installer command-line + (U(http://www.iis.net/learn/install/web-platform-installer/web-platform-installer-v4-command-line-webpicmdexe-rtw-release)). + - Must be installed and present in PATH (see M(chocolatey.chocolatey.win_chocolatey) module; 'webpicmd' is the package name, and you must install + 'lessmsi' first too)? + - Install IIS first (see M(ansible.windows.win_feature) module). +notes: + - Accepts EULAs and suppresses reboot - you will need to check manage reboots yourself (see M(ansible.windows.win_reboot) module) +options: + name: + description: + - Name of the package to be installed. + type: str + required: yes +seealso: +- module: ansible.windows.win_package +author: +- Peter Mounce (@petemounce) +''' + +EXAMPLES = r''' +- name: Install URLRewrite2. + community.windows.win_webpicmd: + name: URLRewrite2 +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_xml.ps1 b/ansible_collections/community/windows/plugins/modules/win_xml.ps1 new file mode 100644 index 000000000..cec1427fa --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_xml.ps1 @@ -0,0 +1,292 @@ +#!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 +#Requires -Module Ansible.ModuleUtils.Backup + +Set-StrictMode -Version 2 + +function Copy-Xml($dest, $src, $xmlorig) { + if ($src.get_NodeType() -eq "Text") { + $dest.set_InnerText($src.get_InnerText()) + } + + if ($src.get_HasAttributes()) { + foreach ($attr in $src.get_Attributes()) { + $dest.SetAttribute($attr.get_Name(), $attr.get_Value()) + } + } + + if ($src.get_HasChildNodes()) { + foreach ($childnode in $src.get_ChildNodes()) { + if ($childnode.get_NodeType() -eq "Element") { + $newnode = $xmlorig.CreateElement($childnode.get_Name(), $xmlorig.get_DocumentElement().get_NamespaceURI()) + Copy-Xml -dest $newnode -src $childnode -xmlorig $xmlorig + $dest.AppendChild($newnode) | Out-Null + } + elseif ($childnode.get_NodeType() -eq "Text") { + $dest.set_InnerText($childnode.get_InnerText()) + } + } + } +} + +function Compare-XmlDoc($actual, $expected) { + if ($actual.get_Name() -ne $expected.get_Name()) { + throw "Actual name not same as expected: actual=" + $actual.get_Name() + ", expected=" + $expected.get_Name() + } + ##attributes... + + if (($actual.get_NodeType() -eq "Element") -and ($expected.get_NodeType() -eq "Element")) { + if ($actual.get_HasAttributes() -and $expected.get_HasAttributes()) { + if ($actual.get_Attributes().Count -ne $expected.get_Attributes().Count) { + throw "attribute mismatch for actual=" + $actual.get_Name() + } + for ($i = 0; $i -lt $expected.get_Attributes().Count; $i = $i + 1) { + if ($expected.get_Attributes()[$i].get_Name() -ne $actual.get_Attributes()[$i].get_Name()) { + throw "attribute name mismatch for actual=" + $actual.get_Name() + } + if ($expected.get_Attributes()[$i].get_Value() -ne $actual.get_Attributes()[$i].get_Value()) { + throw "attribute value mismatch for actual=" + $actual.get_Name() + } + } + } + + if (($actual.get_HasAttributes() -and !$expected.get_HasAttributes()) -or (!$actual.get_HasAttributes() -and $expected.get_HasAttributes())) { + throw "attribute presence mismatch for actual=" + $actual.get_Name() + } + } + + ##children + if ($expected.get_ChildNodes().Count -ne $actual.get_ChildNodes().Count) { + throw "child node mismatch. for actual=" + $actual.get_Name() + } + + for ($i = 0; $i -lt $expected.get_ChildNodes().Count; $i = $i + 1) { + if (-not $actual.get_ChildNodes()[$i]) { + throw "actual missing child nodes. for actual=" + $actual.get_Name() + } + Compare-XmlDoc $expected.get_ChildNodes()[$i] $actual.get_ChildNodes()[$i] + } + + if ($expected.get_InnerText()) { + if ($expected.get_InnerText() -ne $actual.get_InnerText()) { + throw "inner text mismatch for actual=" + $actual.get_Name() + } + } + elseif ($actual.get_InnerText()) { + throw "actual has inner text but expected does not for actual=" + $actual.get_Name() + } +} + + +function Save-ChangedXml($xmlorig, $result, $message, $check_mode, $backup) { + $result.changed = $true + if (-Not $check_mode) { + if ($backup) { + $result.backup_file = Backup-File -path $dest -WhatIf:$check_mode + # Ensure backward compatibility (deprecate in future) + $result.backup = $result.backup_file + } + $xmlorig.Save($dest) + $result.msg = $message + } + else { + $result.msg += " check mode" + } +} + +$params = Parse-Args $args -supports_check_mode $true +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false + +$debug_level = Get-AnsibleParam -obj $params -name "_ansible_verbosity" -type "int" +$debug = $debug_level -gt 2 + +$dest = Get-AnsibleParam $params "path" -type "path" -FailIfEmpty $true -aliases "dest", "file" +$fragment = Get-AnsibleParam $params "fragment" -type "str" -aliases "xmlstring" +$xpath = Get-AnsibleParam $params "xpath" -type "str" -FailIfEmpty $true +$backup = Get-AnsibleParam $params "backup" -type "bool" -Default $false +$type = Get-AnsibleParam $params "type" -type "str" -Default "element" -ValidateSet "element", "attribute", "text" +$attribute = Get-AnsibleParam $params "attribute" -type "str" -FailIfEmpty ($type -eq "attribute") +$state = Get-AnsibleParam $params "state" -type "str" -Default "present" +$count = Get-AnsibleParam $params "count" -type "bool" -Default $false + +$result = @{ + changed = $false +} + +If (-Not (Test-Path -LiteralPath $dest -PathType Leaf)) { + Fail-Json $result "Specified path $dest does not exist or is not a file." +} + +$xmlorig = New-Object -TypeName System.Xml.XmlDocument +$xmlorig.XmlResolver = $null +Try { + $xmlorig.Load($dest) +} +Catch { + Fail-Json $result "Failed to parse file at '$dest' as an XML document: $($_.Exception.Message)" +} + +$namespaceMgr = New-Object System.Xml.XmlNamespaceManager $xmlorig.NameTable +$namespace = $xmlorig.DocumentElement.NamespaceURI +$localname = $xmlorig.DocumentElement.LocalName + +$namespaceMgr.AddNamespace($xmlorig.$localname.SchemaInfo.Prefix, $namespace) + +$nodeList = $xmlorig.SelectNodes($xpath, $namespaceMgr) +$nodeListCount = $nodeList.get_Count() +if ($count) { + $result.count = $nodeListCount + if (-not $fragment) { + Exit-Json $result + } +} +## Exit early if xpath did not match any nodes +if ($nodeListCount -eq 0) { + $result.msg = "The supplied xpath did not match any nodes. If this is unexpected, check your xpath is valid for the xml file at supplied path." + Exit-Json $result +} + +$changed = $false +$result.msg = "not changed" + +if ($type -eq "element") { + if ($state -eq "absent") { + + $removals = [System.Collections.Generic.List[String]]@() + + foreach ($node in $nodeList) { + # there are some nodes that match xpath, delete without comparing them to fragment + if (-Not $check_mode) { + [void]$node.get_ParentNode().RemoveChild($node) + $changed = $true + } + + if ($debug) { + $removals.Add($node.get_OuterXml()) + } + } + + if ($removals) { + $result.removed = $removals -join ", " + } + } + else { + # state -eq 'present' + $xmlfragment = $null + Try { + $xmlfragment = [xml]$fragment + } + Catch { + Fail-Json $result "Failed to parse fragment as XML: $($_.Exception.Message)" + } + + foreach ($node in $nodeList) { + $candidate = $xmlorig.CreateElement($xmlfragment.get_DocumentElement().get_Name(), $xmlorig.get_DocumentElement().get_NamespaceURI()) + Copy-Xml -dest $candidate -src $xmlfragment.DocumentElement -xmlorig $xmlorig + + if ($node.get_NodeType() -eq "Document") { + $node = $node.get_DocumentElement() + } + + if ($node.ChildNodes.Count -eq 0) { + $elements = @($node) + } + else { + $elements = $node.get_ChildNodes() + } + + [bool]$present = $false + [bool]$changed = $false + if ($elements.get_Count()) { + if ($debug) { + $err = @() + $result.err = { $err }.Invoke() + } + foreach ($element in $elements) { + try { + Compare-XmlDoc $candidate $element + $present = $true + break + } + catch { + if ($debug) { + $result.err.Add($_.Exception.ToString()) + } + } + } + if (-Not $present -and ($state -eq "present")) { + [void]$node.AppendChild($candidate) + $result.msg = $result.msg + "xml added " + $changed = $true + } + } + } + } +} +elseif ($type -eq "text") { + foreach ($node in $nodeList) { + if ($node.get_InnerText() -ne $fragment) { + $node.set_InnerText($fragment) + $changed = $true + } + } +} +elseif ($type -eq "attribute") { + foreach ($node in $nodeList) { + if ($state -eq 'present') { + if ($node.NodeType -eq 'Attribute') { + if ($node.Name -eq $attribute -and $node.Value -ne $fragment ) { + # this is already the attribute with the right name, so just set its value to match fragment + $node.Value = $fragment + $changed = $true + } + } + else { + # assume NodeType is Element + if (!$node.HasAttribute($attribute) -or ($node.$attribute -ne $fragment)) { + if (!$node.HasAttribute($attribute)) { + # add attribute to Element if missing + $node.SetAttributeNode($attribute, $xmlorig.get_DocumentElement().get_NamespaceURI()) + } + #set the attribute into the element + $node.SetAttribute($attribute, $fragment) + $changed = $true + } + } + } + elseif ($state -eq 'absent') { + if ($node.NodeType -eq 'Attribute') { + $attrNode = [System.Xml.XmlAttribute]$node + $parent = $attrNode.OwnerElement + $parent.RemoveAttribute($attribute) + $changed = $true + } + else { + # element node processing + if ($node.Name -eq $attribute ) { + # note not caring about the state of 'fragment' at this point + $node.RemoveAttribute($attribute) + $changed = $true + } + } + } + else { + Add-Warning $result "Unexpected state when processing attribute $($attribute), add was $add, state was $state" + } + } +} +if ($changed) { + if ($state -eq "absent") { + $summary = "$type removed" + } + else { + $summary = "$type changed" + } + Save-ChangedXml -xmlorig $xmlorig -result $result -message $summary -check_mode $check_mode -backup $backup +} + +Exit-Json $result diff --git a/ansible_collections/community/windows/plugins/modules/win_xml.py b/ansible_collections/community/windows/plugins/modules/win_xml.py new file mode 100644 index 000000000..a069c7b5d --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_xml.py @@ -0,0 +1,139 @@ +#!/usr/bin/python +# -*- 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) + +DOCUMENTATION = r''' +--- +module: win_xml +short_description: Manages XML file content on Windows hosts +description: + - Manages XML nodes, attributes and text, using xpath to select which xml nodes need to be managed. + - XML fragments, formatted as strings, are used to specify the desired state of a part or parts of XML files on remote Windows servers. + - For non-Windows targets, use the M(community.general.xml) module instead. +options: + attribute: + description: + - The attribute name if the type is 'attribute'. + - Required if C(type=attribute). + type: str + count: + description: + - When set to C(yes), return the number of nodes matched by I(xpath). + type: bool + default: false + 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 + fragment: + description: + - The string representation of the XML fragment expected at xpath. Since ansible 2.9 not required when I(state=absent), or when I(count=yes). + type: str + required: false + aliases: [ xmlstring ] + path: + description: + - Path to the file to operate on. + type: path + required: true + aliases: [ dest, file ] + state: + description: + - Set or remove the nodes (or attributes) matched by I(xpath). + type: str + default: present + choices: [ present, absent ] + type: + description: + - The type of XML node you are working with. + type: str + default: element + choices: [ attribute, element, text ] + xpath: + description: + - Xpath to select the node or nodes to operate on. + type: str + required: true +author: + - Richard Levenberg (@richardcs) + - Jon Hawkesworth (@jhawkesworth) +notes: + - Only supports operating on xml elements, attributes and text. + - Namespace, processing-instruction, command and document node types cannot be modified with this module. +seealso: + - module: community.general.xml + description: XML manipulation for Posix hosts. + - name: w3shools XPath tutorial + description: A useful tutorial on XPath + link: https://www.w3schools.com/xml/xpath_intro.asp +''' + +EXAMPLES = r''' +- name: Apply our filter to Tomcat web.xml + community.windows.win_xml: + path: C:\apache-tomcat\webapps\myapp\WEB-INF\web.xml + fragment: '<filter><filter-name>MyFilter</filter-name><filter-class>com.example.MyFilter</filter-class></filter>' + xpath: '/*' + +- name: Apply sslEnabledProtocols to Tomcat's server.xml + community.windows.win_xml: + path: C:\Tomcat\conf\server.xml + xpath: '//Server/Service[@name="Catalina"]/Connector[@port="9443"]' + attribute: 'sslEnabledProtocols' + fragment: 'TLSv1,TLSv1.1,TLSv1.2' + type: attribute + +- name: remove debug configuration nodes from nlog.conf + community.windows.win_xml: + path: C:\IISApplication\nlog.conf + xpath: /nlog/rules/logger[@name="debug"]/descendant::* + state: absent + +- name: count configured connectors in Tomcat's server.xml + community.windows.win_xml: + path: C:\Tomcat\conf\server.xml + xpath: //Server/Service/Connector + count: yes + register: connector_count + +- name: show connector count + debug: + msg="Connector count is {{connector_count.count}}" + +- name: ensure all lang=en attributes to lang=nl + community.windows.win_xml: + path: C:\Data\Books.xml + xpath: //@[lang="en"] + attribute: lang + fragment: nl + type: attribute + +''' + +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 +count: + description: Number of nodes matched by xpath. + returned: if count=yes + type: int + sample: 33 +msg: + description: What was done. + returned: always + type: str + sample: "xml added" +err: + description: XML comparison exceptions. + returned: always, for type element and -vvv or more + type: list + sample: attribute mismatch for actual=string +''' diff --git a/ansible_collections/community/windows/plugins/modules/win_zip.ps1 b/ansible_collections/community/windows/plugins/modules/win_zip.ps1 new file mode 100644 index 000000000..6bcb62b0e --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_zip.ps1 @@ -0,0 +1,78 @@ +#!powershell + +# Copyright: (c) 2021, Kento Yagisawa <thel.vadam2485@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic + +$spec = @{ + options = @{ + # Need to support \* which type='path' does not, the path is expanded further down. + src = @{ type = 'str'; required = $true } + dest = @{ type = 'path'; required = $true } + } + supports_check_mode = $true +} +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$src = [Environment]::ExpandEnvironmentVariables($module.Params.src) +$dest = $module.Params.dest + +$srcFile = [System.IO.Path]::GetFileName($src) +$compressionLevel = [System.IO.Compression.CompressionLevel]::Optimal +$encoding = New-Object -TypeName System.Text.UTF8Encoding -ArgumentList $false +$srcWildcard = $false + +# If the path ends with '\*' we want to include the dir contents and not the dir itself +If ($src -match '\\\*$') { + $srcWildcard = $true + $src = $src.Substring(0, $src.Length - 2) +} + +If (-not (Test-Path -LiteralPath $src)) { + $module.FailJson("The source file or directory '$src' does not exist.") +} + +If ($dest -notlike "*.zip") { + $module.FailJson("The destination zip file path '$dest' need to be zip file path.") +} + +If (Test-Path -LiteralPath $dest) { + $module.Result.msg = "The destination zip file '$dest' already exists." + $module.ExitJson() +} + +# Check .NET v4.5 or later version exists or not +try { + Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction Stop +} +catch { + $module.FailJson(".NET Framework 4.5 or later version needs to be installed.", $_) +} + +Function Compress-Zip($src, $dest) { + # Disable using backslash for Zip path. This works for .NET 4.6.1 or later + if ([object].Assembly.GetType("System.AppContext")) { + [System.AppContext]::SetSwitch('Switch.System.IO.Compression.ZipFile.UseBackslash', $false) + } + + If (-not $module.CheckMode) { + If (Test-Path -LiteralPath $src -PathType Container) { + [System.IO.Compression.ZipFile]::CreateFromDirectory($src, $dest, $compressionLevel, (-not $srcWildcard), $encoding) + } + Else { + $zip = [System.IO.Compression.ZipFile]::Open($dest, 'Update') + try { + [void][System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, $src, $srcFile, $compressionLevel) + } + finally { + $zip.Dispose() + } + } + } + $module.Result.changed = $true +} + +Compress-Zip -src $src -dest $dest + +$module.ExitJson() diff --git a/ansible_collections/community/windows/plugins/modules/win_zip.py b/ansible_collections/community/windows/plugins/modules/win_zip.py new file mode 100644 index 000000000..53ea14585 --- /dev/null +++ b/ansible_collections/community/windows/plugins/modules/win_zip.py @@ -0,0 +1,53 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Kento Yagisawa <thel.vadam2485@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_zip +short_description: Compress file or directory as zip archive on the Windows node +description: +- Compress file or directory as zip archive. +- For non-Windows targets, use the M(community.general.archive) module instead. +notes: +- The filenames in the zip are encoded using UTF-8. +requirements: +- .NET Framework 4.5 or later +options: + src: + description: + - File or directory path to be zipped (provide absolute path on the target node). + - When a directory path the directory is zipped as the root entry in the archive. + - Specify C(\*) to the end of I(src) to zip the contents of the directory and not the directory itself. + type: str + required: yes + dest: + description: + - Destination path of zip file (provide absolute path of zip file on the target node). + type: path + required: yes +seealso: +- module: community.general.archive +author: +- Kento Yagisawa (@hiyoko_taisa) +''' + +EXAMPLES = r''' +- name: Compress a file + community.windows.win_zip: + src: C:\Users\hiyoko\log.txt + dest: C:\Users\hiyoko\log.zip + +- name: Compress a directory as the root of the archive + community.windows.win_zip: + src: C:\Users\hiyoko\log + dest: C:\Users\hiyoko\log.zip + +- name: Compress the directories contents + community.windows.win_zip: + src: C:\Users\hiyoko\log\* + dest: C:\Users\hiyoko\log.zip + +''' diff --git a/ansible_collections/community/windows/tests/.gitignore b/ansible_collections/community/windows/tests/.gitignore new file mode 100644 index 000000000..ea1472ec1 --- /dev/null +++ b/ansible_collections/community/windows/tests/.gitignore @@ -0,0 +1 @@ +output/ diff --git a/ansible_collections/community/windows/tests/integration/targets/psexec/aliases b/ansible_collections/community/windows/tests/integration/targets/psexec/aliases new file mode 100644 index 000000000..d76b41fdb --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/psexec/aliases @@ -0,0 +1,3 @@ +windows +shippable/windows/group5 + diff --git a/ansible_collections/community/windows/tests/integration/targets/psexec/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/psexec/tasks/main.yml new file mode 100644 index 000000000..3b0471436 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/psexec/tasks/main.yml @@ -0,0 +1,49 @@ +--- +- name: check whether the host supports encryption + ansible.windows.win_shell: | + if ([System.Environment]::OSVersion.Version -lt [Version]"6.2") { + "false" + } else { + "true" + } + register: encryption_supported_raw + +- name: install pypsexec Python library for tests + command: '{{ ansible_python_interpreter | default("python") }} -m pip install pypsexec' + register: psexec_install + changed_when: '"Requirement already satisfied: pypsexec" not in psexec_install.stdout' + delegate_to: localhost + +- name: define psexec variables + set_fact: + psexec_hostname: '{{ansible_host}}' + psexec_username: '{{ansible_user}}' + psexec_password: '{{ansible_password}}' + psexec_encrypt: '{{encryption_supported_raw.stdout_lines[0]|bool}}' + +- name: create test rule to allow SMB traffic inbound + win_firewall_rule: + name: File and Printer Sharing (SMB-In) Test + direction: in + action: allow + localport: 445 + enabled: yes + protocol: tcp + program: System + profiles: + - domain + - private + - public + state: present + +- name: run tests + block: + - include_tasks: tests.yml + + always: + - name: remove test rule that allows SMB traffic inbound + win_firewall_rule: + name: File and Printer Sharing (SMB-In) Test + direction: in + action: allow + state: absent diff --git a/ansible_collections/community/windows/tests/integration/targets/psexec/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/psexec/tasks/tests.yml new file mode 100644 index 000000000..b542cb43e --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/psexec/tasks/tests.yml @@ -0,0 +1,231 @@ +--- +- name: fail when process_password is not set with process_username + psexec: + hostname: '{{psexec_hostname}}' + connection_username: '{{psexec_username}}' + connection_password: '{{psexec_password}}' + encrypt: '{{psexec_encrypt}}' + executable: hostname.exe + process_username: '{{psexec_username}}' + delegate_to: localhost + register: fail_no_process_pass + failed_when: 'fail_no_process_pass.msg != "parameters are required together when not running as System: process_username, process_password"' + +- name: get current host + ansible.windows.win_command: hostname.exe + register: actual_hostname + +- name: run basic psexec command + psexec: + hostname: '{{psexec_hostname}}' + connection_username: '{{psexec_username}}' + connection_password: '{{psexec_password}}' + encrypt: '{{psexec_encrypt}}' + executable: hostname.exe + delegate_to: localhost + register: psexec_hostname_actual + +- name: assert basic psexec command matches expected output + assert: + that: + - psexec_hostname_actual is changed + - psexec_hostname_actual.rc == 0 + - psexec_hostname_actual.stderr == '' + - psexec_hostname_actual.stdout == actual_hostname.stdout + +- name: get output for executable with arguments + ansible.windows.win_command: hostname.exe /? + register: actual_hostname_help + failed_when: actual_hostname_help.rc != 1 + +- name: run psexec command with arguments + psexec: + hostname: '{{psexec_hostname}}' + connection_username: '{{psexec_username}}' + connection_password: '{{psexec_password}}' + encrypt: '{{psexec_encrypt}}' + executable: hostname.exe + arguments: /? + delegate_to: localhost + register: psexec_hostname_help + failed_when: psexec_hostname_help.rc != 1 + +- name: assert basic pesexec command with arguments matches expected output + assert: + that: + - psexec_hostname_help is changed + - psexec_hostname_help.rc == 1 + - psexec_hostname_help.stderr == actual_hostname_help.stderr + - psexec_hostname_help.stdout == actual_hostname_help.stdout + +- name: run psexec command and send data through stdin + psexec: + hostname: '{{psexec_hostname}}' + connection_username: '{{psexec_username}}' + connection_password: '{{psexec_password}}' + encrypt: '{{psexec_encrypt}}' + executable: powershell.exe + arguments: '-' + stdin: | + Write-Host hello world + Write-Host this is another message + exit 0 + delegate_to: localhost + register: psexec_stdin + +- name: assert psexec ommand and send data through stdin + assert: + that: + - psexec_stdin is changed + - psexec_stdin.rc == 0 + - psexec_stdin.stderr == '' + - psexec_stdin.stdout == 'hello world\nthis is another message\n' + +- name: run psexec command with specific process username + psexec: + hostname: '{{psexec_hostname}}' + connection_username: '{{psexec_username}}' + connection_password: '{{psexec_password}}' + encrypt: '{{psexec_encrypt}}' + load_profile: no # on Azure, the profile does not exist yet so we don't load it for this task + executable: powershell.exe + arguments: '-' + stdin: | + ((Get-CimInstance Win32_Process -filter "processid = $pid") | Get-CimAssociatedInstance -Association Win32_SessionProcess).LogonType + exit 0 + process_username: '{{psexec_username}}' + process_password: '{{psexec_password}}' + delegate_to: localhost + register: psexec_process_username + +- name: assert psexec command with specific process username + assert: + that: + - psexec_process_username is changed + - psexec_process_username.rc == 0 + - psexec_process_username.stderr == '' + - psexec_process_username.stdout_lines[0] != '3' # 3 is Network Logon Type, we assert we are not a network logon with process credentials + +- name: run psexec command with both stderr and stdout + psexec: + hostname: '{{psexec_hostname}}' + connection_username: '{{psexec_username}}' + connection_password: '{{psexec_password}}' + encrypt: '{{psexec_encrypt}}' + executable: cmd.exe + arguments: /c echo first && echo second 1>&2 && echo third + delegate_to: localhost + register: psexec_process_stderr + +- name: assert psexec command with both stderr and stdout + assert: + that: + - psexec_process_stderr is changed + - psexec_process_stderr.rc == 0 + - psexec_process_stderr.stderr == 'second \r\n' + - psexec_process_stderr.stdout == 'first \r\nthird\r\n' + +- name: run process asynchronously + psexec: + hostname: '{{psexec_hostname}}' + connection_username: '{{psexec_username}}' + connection_password: '{{psexec_password}}' + encrypt: '{{psexec_encrypt}}' + executable: powershell.exe + arguments: Start-Sleep -Seconds 30 + asynchronous: yes + delegate_to: localhost + register: psexec_process_async + +- name: check if process is still running + ansible.windows.win_shell: (Get-Process -ID {{psexec_process_async.pid}}).ProcessName + register: psexec_process_async_actual + +- name: assert run process asynchronously + assert: + that: + - psexec_process_async is changed + - psexec_process_async.rc is not defined + - psexec_process_async.pid is defined + - psexec_process_async.stdout is not defined + - psexec_process_async.stderr is not defined + - psexec_process_async_actual.stdout_lines[0] == 'powershell' + +- name: run process interactively + psexec: + hostname: '{{psexec_hostname}}' + connection_username: '{{psexec_username}}' + connection_password: '{{psexec_password}}' + encrypt: '{{psexec_encrypt}}' + executable: powershell.exe + arguments: Write-Host hi + interactive: yes + delegate_to: localhost + register: psexec_process_interactive + +- name: assert run process interactively + assert: + that: + - psexec_process_interactive is changed + - psexec_process_interactive.rc == 0 + - psexec_process_interactive.stdout is not defined + - psexec_process_interactive.stderr is not defined + +- name: run process with timeout + psexec: + hostname: '{{psexec_hostname}}' + connection_username: '{{psexec_username}}' + connection_password: '{{psexec_password}}' + encrypt: '{{psexec_encrypt}}' + executable: powershell.exe + arguments: Start-Sleep -Seconds 30 + process_timeout: 5 + delegate_to: localhost + register: psexec_process_timeout + failed_when: psexec_process_timeout.rc == 0 + +- name: assert psexec process with timeout + assert: + that: + - psexec_process_timeout.rc != 0 + - psexec_process_timeout.stdout == '' + - psexec_process_timeout.stderr == '' + +- name: run process as system + psexec: + hostname: '{{psexec_hostname}}' + connection_username: '{{psexec_username}}' + connection_password: '{{psexec_password}}' + encrypt: '{{psexec_encrypt}}' + executable: whoami.exe + process_username: System + delegate_to: localhost + register: psexec_process_system + +- name: assert run process as system + assert: + that: + - psexec_process_system is changed + - psexec_process_system.rc == 0 + - psexec_process_system.stderr == '' + - psexec_process_system.stdout == 'nt authority\system\r\n' + +- name: run process with different chdir + psexec: + hostname: '{{psexec_hostname}}' + connection_username: '{{psexec_username}}' + connection_password: '{{psexec_password}}' + encrypt: '{{psexec_encrypt}}' + executable: powershell.exe + arguments: (pwd).Path + working_directory: C:\Windows + delegate_to: localhost + register: psexec_process_working_dir + +- name: assert run process with different chdir + assert: + that: + - psexec_process_working_dir is changed + - psexec_process_working_dir.rc == 0 + - psexec_process_working_dir.stderr == '' + - psexec_process_working_dir.stdout == 'C:\Windows\r\n' diff --git a/ansible_collections/community/windows/tests/integration/targets/setup_domain_tests/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/setup_domain_tests/tasks/main.yml new file mode 100644 index 000000000..b46d69078 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/setup_domain_tests/tasks/main.yml @@ -0,0 +1,61 @@ +--- +- name: Change the hostname to ansible-tester + ansible.windows.win_hostname: + name: ansible-tester + register: rename_host + +- name: Reboot after changing hostname + ansible.windows.win_reboot: + when: rename_host.reboot_required + +- name: Ensure the ActiveDirectory module is installed + ansible.windows.win_feature: + name: + - RSAT-AD-PowerShell + state: present + +- name: Ensure domain is present + ansible.windows.win_domain: + dns_domain_name: ansible.test + safe_mode_password: password123! + register: ensure_domain + +- name: Reboot after domain promotion + ansible.windows.win_reboot: + when: ensure_domain.reboot_required + +# While usually win_reboot waits until it is fully done before continuing I've seen Server 2019 in CI still waiting +# for things to initialise. By tested if ADWS is available with a simple check we can ensure the host is at least +# ready to test AD. Typically I've found it takes about 60 retries so doubling it should cover even an absolute worst +# case. +- name: Post reboot test for ADWS to come online + ansible.windows.win_powershell: + parameters: + Delay: 5 + Retries: 120 + script: | + [CmdletBinding()] + param ( + [int]$Delay, + [int]$Retries + ) + $Ansible.Changed = $false + $attempts = 0 + $err = $null + while ($true) { + $attempts++ + try { + Get-ADRootDSE -ErrorAction Stop + break + } + catch { + if ($attempts -eq $Retries) { + throw + } + Start-Sleep -Seconds $Delay + } + } + $attempts + become: yes + become_method: runas + become_user: SYSTEM diff --git a/ansible_collections/community/windows/tests/integration/targets/setup_http_tests/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/setup_http_tests/defaults/main.yml new file mode 100644 index 000000000..a1e5b8d10 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/setup_http_tests/defaults/main.yml @@ -0,0 +1,4 @@ +badssl_host: wrong.host.badssl.com +httpbin_host: httpbin.org +sni_host: ci-files.testing.ansible.com +badssl_host_substring: wrong.host.badssl.com diff --git a/ansible_collections/community/windows/tests/integration/targets/setup_http_tests/handlers/main.yml b/ansible_collections/community/windows/tests/integration/targets/setup_http_tests/handlers/main.yml new file mode 100644 index 000000000..a14627931 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/setup_http_tests/handlers/main.yml @@ -0,0 +1,6 @@ +- name: remove CA trust store cert + ansible.windows.win_certificate_store: + thumbprint: '{{ httptester_ca_cert_info.thumbprints[0] }}' + state: absent + store_location: LocalMachine + store_name: Root diff --git a/ansible_collections/community/windows/tests/integration/targets/setup_http_tests/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/setup_http_tests/meta/main.yml new file mode 100644 index 000000000..1810d4bec --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/setup_http_tests/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_remote_tmp_dir diff --git a/ansible_collections/community/windows/tests/integration/targets/setup_http_tests/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/setup_http_tests/tasks/main.yml new file mode 100644 index 000000000..d4f1d789b --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/setup_http_tests/tasks/main.yml @@ -0,0 +1,42 @@ +# The docker --link functionality gives us an ENV var we can key off of to see if we have access to +# the httptester container +- set_fact: + has_httptester: "{{ lookup('env', 'HTTPTESTER') != '' }}" + +- name: If we are running with access to a httptester container, grab it's cacert and install it + when: has_httptester | bool + block: + - name: Override hostname defaults with httptester linked names + include_vars: httptester.yml + + - name: make sure the port forwarder is active + ansible.windows.win_wait_for: + host: ansible.http.tests + port: 80 + state: started + timeout: 300 + + - name: get client cert/key + ansible.windows.win_get_url: + url: http://ansible.http.tests/{{ item }} + dest: '{{ remote_tmp_dir }}\{{ item }}' + register: win_download + retries: 5 # Just have a retry in case the host is running a bit slower today. + until: win_download is successful + with_items: + - client.pem + - client.key + + - name: retrieve test cacert + ansible.windows.win_get_url: + url: http://ansible.http.tests/cacert.pem + dest: '{{ remote_tmp_dir }}\cacert.pem' + + - name: update ca trust + ansible.windows.win_certificate_store: + path: '{{ remote_tmp_dir }}\cacert.pem' + state: present + store_location: LocalMachine + store_name: Root + register: httptester_ca_cert_info + notify: remove CA trust store cert diff --git a/ansible_collections/community/windows/tests/integration/targets/setup_http_tests/vars/httptester.yml b/ansible_collections/community/windows/tests/integration/targets/setup_http_tests/vars/httptester.yml new file mode 100644 index 000000000..0e23ae936 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/setup_http_tests/vars/httptester.yml @@ -0,0 +1,5 @@ +# these are fake hostnames provided by docker link for the httptester container +badssl_host: fail.ansible.http.tests +httpbin_host: ansible.http.tests +sni_host: sni1.ansible.http.tests +badssl_host_substring: HTTP Client Testing Service diff --git a/ansible_collections/community/windows/tests/integration/targets/setup_remote_tmp_dir/handlers/main.yml b/ansible_collections/community/windows/tests/integration/targets/setup_remote_tmp_dir/handlers/main.yml new file mode 100644 index 000000000..f0f0ee5ee --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/setup_remote_tmp_dir/handlers/main.yml @@ -0,0 +1,4 @@ +- name: delete temporary directory + ansible.windows.win_file: + path: '{{ remote_tmp_dir }}' + state: absent diff --git a/ansible_collections/community/windows/tests/integration/targets/setup_remote_tmp_dir/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/setup_remote_tmp_dir/tasks/main.yml new file mode 100644 index 000000000..4b6e1395b --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/setup_remote_tmp_dir/tasks/main.yml @@ -0,0 +1,11 @@ +- name: create temporary directory + ansible.windows.win_tempfile: + state: directory + suffix: .test + register: remote_tmp_dir + notify: + - delete temporary directory + +- name: record temporary directory + set_fact: + remote_tmp_dir: "{{ remote_tmp_dir.path }}" diff --git a/ansible_collections/community/windows/tests/integration/targets/setup_win_device/handlers/main.yml b/ansible_collections/community/windows/tests/integration/targets/setup_win_device/handlers/main.yml new file mode 100644 index 000000000..5c01331ad --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/setup_win_device/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: remove dummy network adapter device + win_device: + name: '{{ network_device_name_raw.name }}' + state: absent diff --git a/ansible_collections/community/windows/tests/integration/targets/setup_win_device/library/win_device.ps1 b/ansible_collections/community/windows/tests/integration/targets/setup_win_device/library/win_device.ps1 new file mode 100644 index 000000000..77fac9086 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/setup_win_device/library/win_device.ps1 @@ -0,0 +1,546 @@ +#!powershell + +#AnsibleRequires -CSharpUtil Ansible.Basic +#Requires -Module Ansible.ModuleUtils.AddType + +$spec = @{ + options = @{ + hardware_id = @{ type = "str" } + name = @{ type = "str" } + path = @{ type = "path" } + state = @{ type = "str"; choices = @("absent", "present"); default = "present" } + } + required_if = @( + @("state", "present", @("path", "hardware_id"), $true), + @("state", "absent", @(, "name")) + ) + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$hardware_id = $module.Params.hardware_id +$name = $module.Params.name +$path = $module.Params.path +$state = $module.Params.state + +$module.Result.reboot_required = $false + +Add-CSharpType -References @' +using Microsoft.Win32.SafeHandles; +using System; +using System.ComponentModel; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ansible.Device +{ + public class NativeHelpers + { + [StructLayout(LayoutKind.Sequential)] + public class SP_DEVINFO_DATA + { + public UInt32 cbSize; + public Guid ClassGuid; + public UInt32 DevInst; + public IntPtr Reserved; + + public SP_DEVINFO_DATA() + { + this.cbSize = (UInt32)Marshal.SizeOf(this); + this.ClassGuid = Guid.Empty; + } + } + + [Flags] + public enum DeviceInfoCreationFlags : uint + { + DICD_GENERATE_ID = 0x00000001, + DICD_INHERIT_CLASSDRVS = 0x00000002, + } + + public enum DeviceProperty : uint + { + SPDRP_DEVICEDESC = 0x0000000, + SPDRP_HARDWAREID = 0x0000001, + SPDRP_COMPATIBLEIDS = 0x0000002, + SPDRP_UNUSED0 = 0x0000003, + SPDRP_SERVICE = 0x0000004, + SPDRP_UNUSED1 = 0x0000005, + SPDRP_UNUSED2 = 0x0000006, + SPDRP_CLASS = 0x0000007, // Read only - tied to ClassGUID + SPDRP_CLASSGUID = 0x0000008, + SPDRP_DRIVER = 0x0000009, + SPDRP_CONFIGFLAGS = 0x000000a, + SPDRP_MFG = 0x000000b, + SPDRP_FRIENDLYNAME = 0x000000c, + SPDRP_LOCATION_INFORMATION = 0x000000d, + SPDRP_PHYSICAL_DEVICE_OBJECT_NAME = 0x000000e, // Read only + SPDRP_CAPABILITIES = 0x000000f, // Read only + SPDRP_UI_NUMBER = 0x0000010, // Read only + SPDRP_UPPERFILTERS = 0x0000011, + SPDRP_LOWERFILTERS = 0x0000012, + SPDRP_BUSTYPEGUID = 0x0000013, // Read only + SPDRP_LEGACYBUSTYPE = 0x0000014, // Read only + SPDRP_BUSNUMBER = 0x0000015, // Read only + SPDRP_ENUMERATOR_NAME = 0x0000016, // Read only + SPDRP_SECURITY = 0x0000017, + SPDRP_SECURITY_SDS = 0x0000018, + SPDRP_DEVTYPE = 0x0000019, + SPDRP_EXCLUSIVE = 0x000001a, + SPDRP_CHARACTERISTICS = 0x000001b, + SPDRP_ADDRESS = 0x000001c, // Read only + SPDRP_UI_NUMBER_DESC_FORMAT = 0x000001d, + SPDRP_DEVICE_POWER_DATA = 0x000001e, // Read only + SPDRP_REMOVAL_POLICY = 0x000001f, // Read only + SPDRP_REMOVAL_POLICY_HW_DEFAULT = 0x0000020, // Read only + SPDRP_REMOVAL_POLICY_OVERRIDE = 0x0000021, + SPDRP_INSTALL_STATE = 0x0000022, // Read only + SPDRP_LOCATION_PATHS = 0x0000023, // Read only + SPDRP_BASE_CONTAINERID = 0x0000024, // Read only + } + + // https://docs.microsoft.com/en-us/previous-versions/ff549793%28v%3dvs.85%29 + public enum DifCodes : uint + { + DIF_SELECTDIVE = 0x00000001, + DIF_INSTALLDEVICE = 0x00000002, + DIF_ASSIGNRESOURCES = 0x00000003, + DIF_PROPERTIES = 0x00000004, + DIF_REMOVE = 0x00000005, + DIF_FIRSTTIMESETUP = 0x00000006, + DIF_FOUNDDEVICE = 0x00000007, + DIF_SELECTCLASSDRIVERS = 0x00000008, + DIF_VALIDATECLASSDRIVERS = 0x00000009, + DIF_INSTALLCLASSDRIVERS = 0x0000000a, + DIF_CALCDISKSPACE = 0x0000000b, + DIF_DESTROYPRIVATEDATA = 0x0000000c, + DIF_VALIDATEDRIVER = 0x0000000d, + DIF_DETECT = 0x0000000f, + DIF_INSTALLWIZARD = 0x00000010, + DIF_DESTROYWIZARDDATA = 0x00000011, + DIF_PROPERTYCHANGE = 0x00000012, + DIF_ENABLECLASS = 0x00000013, + DIF_DETECTVERIFY = 0x00000014, + DIF_INSTALLDEVICEFILES = 0x00000015, + DIF_UNREMOVE = 0x00000016, + DIF_SELECTBESTCOMPATDRV = 0x00000017, + DIF_ALLOW_INSTALL = 0x00000018, + DIF_REGISTERDEVICE = 0x00000019, + DIF_NEWDEVICEWIZARD_PRESELECT = 0x0000001a, + DIF_NEWDEVICEWIZARD_SELECT = 0x0000001b, + DIF_NEWDEVICEWIZARD_PREANALYZE = 0x0000001c, + DIF_NEWDEVICEWIZARD_POSTANALYZE = 0x0000001d, + DIF_NEWDEVICEWIZARD_FINISHINSTALL = 0x0000001e, + DIF_UNUSED1 = 0x0000001e, + DIF_INSTALLINTERFACES = 0x00000020, + DIF_DETECTCANCEL = 0x00000021, + DIF_REGISTER_COINSTALLERS = 0x00000022, + DIF_ADDPROPERTYPAGE_ADVANCED = 0x00000023, + DIF_ADDPROPERTYPAGE_BASIC = 0x00000024, + DIF_RESERVED1 = 0x00000025, + DIF_TROUBLESHOOTER = 0x00000026, + DIF_POWERMESSAGEWAKE = 0x00000027, + DIF_ADDREMOTEPROPERTYPAGE_ADVANCED = 0x00000028, + DIF_UPDATEDRIVER_UI = 0x00000029, + DIF_FINISHINSTALL_ACTION = 0x0000002a, + } + + [Flags] + public enum GetClassFlags : uint + { + DIGCF_DEFAULT = 0x00000001, + DIGCF_PRESENT = 0x00000002, + DIGCF_ALLCLASSES = 0x00000004, + DIGCF_PROFILE = 0x00000008, + DIGCF_DEVICEINTERFACE = 0x00000010, + } + + [Flags] + public enum InstallFlags : uint + { + INSTALLFLAG_FORCE = 0x00000001, + INSTALLFLAG_READONLY = 0x00000002, + INSTALLFLAG_NONINTERACTIVE = 0x00000004, + INSTALLFLAG_BITS = 0x00000007, + } + } + + public class NativeMethods + { + [DllImport("Setupapi.dll", SetLastError = true)] + public static extern bool SetupDiCallClassInstaller( + NativeHelpers.DifCodes InstallFunction, + SafeDeviceInfoSet DeviceInfoSet, + NativeHelpers.SP_DEVINFO_DATA DeviceInfoData); + + [DllImport("Setupapi.dll", SetLastError = true)] + public static extern SafeDeviceInfoSet SetupDiCreateDeviceInfoList( + Guid ClassGuid, + IntPtr hwndParent); + + [DllImport("Setupapi.dll", SetLastError = true)] + public static extern bool SetupDiCreateDeviceInfoW( + SafeDeviceInfoSet DeviceInfoSet, + [MarshalAs(UnmanagedType.LPWStr)] string DeviceName, + Guid ClassGuid, + [MarshalAs(UnmanagedType.LPWStr)] string DeviceDescription, + IntPtr hwndParent, + NativeHelpers.DeviceInfoCreationFlags CreationFlags, + NativeHelpers.SP_DEVINFO_DATA DeviceInfoData); + + [DllImport("Setupapi.dll", SetLastError = true)] + public static extern bool SetupDiDestroyDeviceInfoList( + IntPtr DeviceInfoSet); + + [DllImport("Setupapi.dll", SetLastError = true)] + public static extern bool SetupDiEnumDeviceInfo( + SafeDeviceInfoSet DeviceInfoSet, + UInt32 MemberIndex, + NativeHelpers.SP_DEVINFO_DATA DeviceInfoData); + + [DllImport("Setupapi.dll", SetLastError = true)] + public static extern SafeDeviceInfoSet SetupDiGetClassDevsW( + Guid ClassGuid, + [MarshalAs(UnmanagedType.LPWStr)] string Enumerator, + IntPtr hwndParent, + NativeHelpers.GetClassFlags Flags); + + [DllImport("Setupapi.dll", SetLastError = true)] + public static extern bool SetupDiGetDeviceRegistryPropertyW( + SafeDeviceInfoSet DeviceInfoSet, + NativeHelpers.SP_DEVINFO_DATA DeviceInfoData, + NativeHelpers.DeviceProperty Property, + out UInt32 PropertyRegDataType, + SafeMemoryBuffer PropertyBuffer, + UInt32 PropertyBufferSize, + ref UInt32 RequiredSize); + + [DllImport("Setupapi.dll", SetLastError = true)] + public static extern bool SetupDiGetINFClassW( + [MarshalAs(UnmanagedType.LPWStr)] string InfName, + ref Guid ClassGuid, + [MarshalAs(UnmanagedType.LPWStr)] StringBuilder ClassName, + UInt32 ClassNameSize, + ref UInt32 RequiredSize); + + [DllImport("Setupapi.dll", SetLastError = true)] + public static extern bool SetupDiSetDeviceRegistryPropertyW( + SafeDeviceInfoSet DeviceInfoSet, + NativeHelpers.SP_DEVINFO_DATA DeviceInfoData, + NativeHelpers.DeviceProperty Property, + SafeMemoryBuffer PropertyBuffer, + UInt32 PropertyBufferSize); + + [DllImport("Newdev.dll", SetLastError = true)] + public static extern bool UpdateDriverForPlugAndPlayDevicesW( + IntPtr hwndParent, + [MarshalAs(UnmanagedType.LPWStr)] string HardwareId, + [MarshalAs(UnmanagedType.LPWStr)] string FullInfPath, + NativeHelpers.InstallFlags InstallFlags, + ref bool bRebootRequired); + } + + public class SafeDeviceInfoSet : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeDeviceInfoSet() : base(true) { } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + protected override bool ReleaseHandle() + { + return NativeMethods.SetupDiDestroyDeviceInfoList(handle); + } + } + + public class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid + { + public int Length = 0; + + public SafeMemoryBuffer() : base(true) { } + + public SafeMemoryBuffer(int cb) : base(true) + { + Length = cb; + base.SetHandle(Marshal.AllocHGlobal(cb)); + } + + public SafeMemoryBuffer(string sz) : base(true) + { + Length = sz.Length * sizeof(char); + base.SetHandle(Marshal.StringToHGlobalUni(sz)); + } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + protected override bool ReleaseHandle() + { + Marshal.FreeHGlobal(handle); + return true; + } + } + + public class DeviceUtil + { + public static string GetDeviceFriendlyName(SafeDeviceInfoSet devInfoSet, NativeHelpers.SP_DEVINFO_DATA devInfo) + { + string friendlyName = GetDeviceStringProp(devInfoSet, devInfo, NativeHelpers.DeviceProperty.SPDRP_FRIENDLYNAME); + + // Older Windows versions may not have a friendly name set. This seems to be the case when the device has + // a unique description so we fallback to that value. + if (null == friendlyName) + friendlyName = GetDeviceStringProp(devInfoSet, devInfo, NativeHelpers.DeviceProperty.SPDRP_DEVICEDESC); + + return friendlyName; + } + + public static void SetDeviceHardwareId(SafeDeviceInfoSet devInfoSet, NativeHelpers.SP_DEVINFO_DATA devInfo, + string hardwareId) + { + SetDeviceStringProp(devInfoSet, devInfo, NativeHelpers.DeviceProperty.SPDRP_HARDWAREID, hardwareId); + } + + private static string GetDeviceStringProp(SafeDeviceInfoSet devInfoSet, NativeHelpers.SP_DEVINFO_DATA devInfo, + NativeHelpers.DeviceProperty property) + { + using (SafeMemoryBuffer memBuf = GetDeviceProperty(devInfoSet, devInfo, property)) + { + if (memBuf.IsInvalid) // Property does not exist so just return null. + return null; + + return Marshal.PtrToStringUni(memBuf.DangerousGetHandle()); + } + } + + private static SafeMemoryBuffer GetDeviceProperty(SafeDeviceInfoSet devInfoSet, + NativeHelpers.SP_DEVINFO_DATA devInfo, NativeHelpers.DeviceProperty property) + { + UInt32 requiredSize = 0; + UInt32 regDataType = 0; + if (!NativeMethods.SetupDiGetDeviceRegistryPropertyW(devInfoSet, devInfo, property, + out regDataType, new SafeMemoryBuffer(0), 0, ref requiredSize)) + { + int errCode = Marshal.GetLastWin32Error(); + if (errCode == 0x0000000D) // ERROR_INVALID_DATA + return new SafeMemoryBuffer(); // The FRIENDLYNAME property does not exist + else if (errCode != 0x0000007A) // ERROR_INSUFFICIENT_BUFFER + throw new Win32Exception(errCode); + } + + SafeMemoryBuffer memBuf = new SafeMemoryBuffer((int)requiredSize); + if (!NativeMethods.SetupDiGetDeviceRegistryPropertyW(devInfoSet, devInfo, property, + out regDataType, memBuf, requiredSize, ref requiredSize)) + { + int errCode = Marshal.GetLastWin32Error(); + memBuf.Dispose(); + + throw new Win32Exception(errCode); + } + + return memBuf; + } + + private static void SetDeviceStringProp(SafeDeviceInfoSet devInfoSet, NativeHelpers.SP_DEVINFO_DATA devInfo, + NativeHelpers.DeviceProperty property, string value) + { + using (SafeMemoryBuffer buffer = new SafeMemoryBuffer(value)) + SetDeviceProperty(devInfoSet, devInfo, property, buffer); + } + + private static void SetDeviceProperty(SafeDeviceInfoSet devInfoSet, NativeHelpers.SP_DEVINFO_DATA devInfo, + NativeHelpers.DeviceProperty property, SafeMemoryBuffer buffer) + { + if (!NativeMethods.SetupDiSetDeviceRegistryPropertyW(devInfoSet, devInfo, property, buffer, + (UInt32)buffer.Length)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } + } +} +'@ + +Function Get-Win32ErrorMessage { + Param ([System.Int32]$ErrorCode) + + $exp = New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList $ErrorCode + return ("{0} (Win32 ErrorCode {1} - 0x{1:X8}" -f $exp.Message, $ErrorCode) +} + +# Determine if the device is already installed +$dev_info_set = [Ansible.Device.NativeMethods]::SetupDiGetClassDevsW( + [Guid]::Empty, + [NullString]::Value, + [System.IntPtr]::Zero, + [Ansible.Device.NativeHelpers+GetClassFlags]::DIGCF_ALLCLASSES +); $err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() + +try { + if ($dev_info_set.IsInvalid) { + $msg = Get-Win32ErrorMessage -ErrorCode $err + $module.FailJson("Failed to get device information set for installed devices: $msg") + } + + $dev_info = $null + if ($null -ne $name) { + # Loop through the set of all devices and compare the name + $idx = 0 + while ($true) { + $dev_info = New-Object -TypeName Ansible.Device.NativeHelpers+SP_DEVINFO_DATA + $res = [Ansible.Device.NativeMethods]::SetupDiEnumDeviceInfo( + $dev_info_set, + $idx, + $dev_info + ); $err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() + + if (-not $res) { + $dev_info = $null + if ($err -eq 0x00000103) { + # ERROR_NO_MORE_ITEMS + break + } + + $msg = Get-Win32ErrorMessage -ErrorCode $err + $module.FailJson("Failed to enumerate device information set at index $($idx): $msg") + } + + $device_name = [Ansible.Device.DeviceUtil]::GetDeviceFriendlyName($dev_info_set, $dev_info) + if ($device_name -eq $name) { + break + } + + $dev_info = $null + $idx++ + } + } + + if ($state -eq "absent" -and $null -ne $dev_info) { + if (-not $module.CheckMode) { + $res = [Ansible.Device.NativeMethods]::SetupDiCallClassInstaller( + [Ansible.Device.NativeHelpers+DifCodes]::DIF_REMOVE, + $dev_info_set, + $dev_info + ); $err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() + + if (-not $res) { + $msg = Get-Win32ErrorMessage -ErrorCode $err + $module.FailJson("Failed to remove device $($name): $msg") + } + } + + $module.Result.changed = $true + } + elseif ($state -eq "present" -and $null -eq $dev_info) { + # Populate the class guid and display name if the path to an inf file was set. + $class_id = [Guid]::Empty + $class_name = $null + if ($path) { + if (-not (Test-Path -LiteralPath $path)) { + $module.FailJson("Could not find the inf file specified at '$path'") + } + + $class_name_sb = New-Object -TypeName System.Text.StringBuilder -ArgumentList 32 # MAX_CLASS_NAME_LEN + $required_size = 0 + $res = [Ansible.Device.NativeMethods]::SetupDiGetINFClassW( + $path, + [ref]$class_id, + $class_name_sb, + $class_name_sb.Capacity, + [ref]$required_size + ); $err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() + + if (-not $res) { + $msg = Get-Win32ErrorMessage -ErrorCode $err + $module.FailJson("Failed to parse driver inf at '$path': $msg") + } + + $class_name = $class_name_sb.ToString() + } + + # When creating a new device we want to start with a blank device information set. + $dev_info_set.Dispose() + + $dev_info_set = [Ansible.Device.NativeMethods]::SetupDiCreateDeviceInfoList( + $class_id, + [System.IntPtr]::Zero + ); $err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() + + if ($dev_info_set.IsInvalid) { + $msg = Get-Win32ErrorMessage -ErrorCode $err + $module.FailJson("Failed to create device info set for the class $($class_id): $msg") + } + + # Create the new device element and add it to the device info set + $dev_info = New-Object -TypeName Ansible.Device.NativeHelpers+SP_DEVINFO_DATA + $res = [Ansible.Device.NativeMethods]::SetupDiCreateDeviceInfoW( + $dev_info_set, + $class_name, + $class_id, + $null, + [System.IntPtr]::Zero, + [Ansible.Device.NativeHelpers+DeviceInfoCreationFlags]::DICD_GENERATE_ID, + $dev_info + ); $err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() + + if (-not $res) { + $msg = Get-Win32ErrorMessage -ErrorCode $err + $module.FailJson("Failed to create new device element for class $($class_name): $msg") + } + + # Set the hardware id of the new device so we can load the proper driver. + [Ansible.Device.DeviceUtil]::SetDeviceHardwareId($dev_info_set, $dev_info, $hardware_id) + + if (-not $module.CheckMode) { + # Install the device + $res = [Ansible.Device.NativeMethods]::SetupDiCallClassInstaller( + [Ansible.Device.NativeHelpers+DifCodes]::DIF_REGISTERDEVICE, + $dev_info_set, + $dev_info + ); $err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() + + if (-not $res) { + $msg = Get-Win32ErrorMessage -ErrorCode $err + $module.FailJson("Failed to register new device for class $($class_name): $msg") + } + + # Load the drivers for the new device + $reboot_required = $false + $res = [Ansible.Device.NativeMethods]::UpdateDriverForPlugAndPlayDevicesW( + [System.IntPtr]::Zero, + $hardware_id, + $path, + [Ansible.Device.NativeHelpers+InstallFlags]'INSTALLFLAG_FORCE, INSTALLFLAG_NONINTERACTIVE', + [ref]$reboot_required + ); $err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() + + if (-not $res) { + # On a failure make sure we cleanup the "installed" device + [Ansible.Device.NativeMethods]::SetupDiCallClassInstaller( + [Ansible.Device.NativeHelpers+DifCodes]::DIF_REMOVE, + $dev_info_set, + $dev_info + ) > $null + + $msg = Get-Win32ErrorMessage -ErrorCode $err + $module.FailJson("Failed to update device driver: $msg") + } + + $module.Result.reboot_required = $reboot_required + + # Now get the name of the newly created device which we return back to Ansible. + $name = [Ansible.Device.DeviceUtil]::GetDeviceFriendlyName($dev_info_set, $dev_info) + } + else { + # Generate random name for check mode output + $name = "Check mode generated device for $($class_name)" + } + $module.Result.changed = $true + } +} +finally { + $dev_info_set.Dispose() +} + +$module.Result.name = $name + +$module.ExitJson() + diff --git a/ansible_collections/community/windows/tests/integration/targets/setup_win_device/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/setup_win_device/tasks/main.yml new file mode 100644 index 000000000..9bfe36fcf --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/setup_win_device/tasks/main.yml @@ -0,0 +1,22 @@ +# Creates a network adapter device for testing purposes and registers the following vars +# network_device_name: The name of the network device +# network_adapter_name: The name of the network adapter +--- +- name: create dummy network adapter device + win_device: + path: '%WinDir%\Inf\netloop.inf' + hardware_id: '*msloop' + state: present + register: network_device_name_raw + notify: remove dummy network adapter device + +- set_fact: + network_device_name: '{{ network_device_name_raw.name }}' + +- name: get name of the dummy network adapter + ansible.windows.win_shell: (Get-CimInstance -Class Win32_NetworkAdapter -Filter "Name='{{ network_device_name }}'").NetConnectionID + changed_when: False + register: network_adapter_name_raw + +- set_fact: + network_adapter_name: '{{ network_adapter_name_raw.stdout | trim }}' diff --git a/ansible_collections/community/windows/tests/integration/targets/setup_win_psget/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/setup_win_psget/meta/main.yml new file mode 100644 index 000000000..45806c8dc --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/setup_win_psget/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_remote_tmp_dir
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/setup_win_psget/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/setup_win_psget/tasks/main.yml new file mode 100644 index 000000000..ce1607647 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/setup_win_psget/tasks/main.yml @@ -0,0 +1,129 @@ +# Installs PackageManagement and PowerShellGet to the required versions for testing +--- +- name: check if PackageManagement has been installed + ansible.windows.win_powershell: + script: | + $ErrorActionPreference = 'Stop' + $Ansible.Changed = $false + + if (-not (Get-Command -Name Install-Module -ErrorAction SilentlyContinue)) { + [PSCustomObject]@{ + Install = $true + Action = "scratch" + } + return + } + + $psGet = Get-Module -Name PowerShellGet -ListAvailable | + Sort-Object -Property Version -Descending | + Select-Object -First 1 -ExpandProperty Version + $package = Get-Module -Name PackageManagement -ListAvailable | + Sort-Object -Property Version -Descending | + Select-Object -First 1 -ExpandProperty Version + + if ($psGet -lt [Version]"1.6.0" -or $package -lt [Version]"1.1.7") { + [PSCustomObject]@{ + Install = $true + Action = "module" + } + } + else { + [PSCustomObject]@{ + Install = $false + } + } + + register: module_installed + +- name: bootstrap required modules + when: module_installed.output[0].Install + block: + - name: install PackageManagement for older hosts + ansible.windows.win_package: + path: https://ansible-ci-files.s3.amazonaws.com/test/integration/targets/setup_win_psget/PackageManagement_x64.msi + product_id: '{57E5A8BB-41EB-4F09-B332-B535C5954A28}' + state: present + when: module_installed.output[0].Action == "scratch" + register: download_res + until: download_res is successful + retries: 3 + delay: 5 + + - name: remove the old versions of PackageManagement and PowerShellGet + ansible.windows.win_file: + path: C:\Program Files\WindowsPowerShell\Modules\{{ item }} + state: absent + when: module_installed.output[0].Action == "scratch" + loop: + - PackageManagement + - PowerShellGet + + - name: create the required folder for nuget + ansible.windows.win_file: + path: C:\Program Files\PackageManagement\ProviderAssemblies\nuget\2.8.5.208 + state: directory + + - name: download nuget provider dll + ansible.windows.win_get_url: + url: https://ansible-ci-files.s3.amazonaws.com/test/integration/targets/setup_win_psget/Microsoft.PackageManagement.NuGetProvider-2.8.5.208.dll + dest: C:\Program Files\PackageManagement\ProviderAssemblies\nuget\2.8.5.208\Microsoft.PackageManagement.NuGetProvider.dll + force: false + register: nuget_download_res + until: nuget_download_res is successful + retries: 3 + delay: 5 + + - name: download newer PackageManagement and PowerShellGet nupkg + ansible.windows.win_get_url: + url: '{{ item.url }}' + dest: '{{ remote_tmp_dir }}\{{ item.name }}.{{ "nupkg" if module_installed.output[0].Action == "module" else "zip" }}' # .zip is required for win_unzip + when: module_installed.output[0].Install + register: download_res + until: download_res is successful + retries: 3 + delay: 5 + loop: + - name: PackageManagement + url: https://ansible-ci-files.s3.amazonaws.com/test/integration/targets/setup_win_psget/packagemanagement.1.1.7.nupkg + - name: PowerShellGet + url: https://ansible-ci-files.s3.amazonaws.com/test/integration/targets/setup_win_psget/powershellget.1.6.0.nupkg + + - name: extract new modules to correct location for older hosts + win_unzip: + src: '{{ remote_tmp_dir }}\{{ item }}.zip' + dest: C:\Program Files\WindowsPowerShell\Modules\{{ item }} + when: module_installed.output[0].Action == "scratch" + loop: + - PackageManagement + - PowerShellGet + + - name: update PackageManagement and PowerShellGet + when: module_installed.output[0].Action == "module" + block: + - name: register local PSRepo + ansible.windows.win_powershell: + script: | + param($Path) + + Register-PSRepository -Name LocalNuget -SourceLocation $Path + parameters: + Path: '{{ remote_tmp_dir }}' + + - name: ensure PowerShellGet and PackageManagement requirements have been met + win_psmodule: + name: PowerShellGet + repository: LocalNuget + accept_license: true + state: present + + always: + - name: unregister local PSRepo + ansible.windows.win_powershell: + script: | + if (Get-PSRepository -Name LocalNuget -ErrorAction SilentlyContinue) { + Unregister-PSRepository -Name LocalNuget + $Ansible.Changed = $true + } + else { + $Ansible.Changed = $false + } diff --git a/ansible_collections/community/windows/tests/integration/targets/win_audit_policy_system/aliases b/ansible_collections/community/windows/tests/integration/targets/win_audit_policy_system/aliases new file mode 100644 index 000000000..3cf5b97e8 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_audit_policy_system/aliases @@ -0,0 +1 @@ +shippable/windows/group3 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_audit_policy_system/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_audit_policy_system/defaults/main.yml new file mode 100644 index 000000000..9e0d35c77 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_audit_policy_system/defaults/main.yml @@ -0,0 +1,3 @@ +#important that the subcategory is from a different category +category_name: detailed tracking +subcategory_name: file system diff --git a/ansible_collections/community/windows/tests/integration/targets/win_audit_policy_system/tasks/add.yml b/ansible_collections/community/windows/tests/integration/targets/win_audit_policy_system/tasks/add.yml new file mode 100644 index 000000000..75ea23045 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_audit_policy_system/tasks/add.yml @@ -0,0 +1,108 @@ +######################## +### check mode apply ### +######################## +- name: check mode enable category + win_audit_policy_system: + category: "{{ category_name }}" + audit_type: success + check_mode: yes + register: category + +- name: check mode enable subcategory + win_audit_policy_system: + subcategory: "{{ subcategory_name }}" + audit_type: success, failure + check_mode: yes + register: subcategory + +- name: check mode assert that changed is true + assert: + that: + - category is changed + - subcategory is changed + +- name: check mode assert that audit_type is "no auditing" + assert: + that: + - item == "no auditing" + with_items: + - "{{ subcategory.current_audit_policy.values() | list }}" + - "{{ category.current_audit_policy.values() | list | unique }}" + +#alternative check for category...pretty noise and requires more lines +# - name: assert that audit_type is no auditing +# assert: +# that: item.value == "no auditing" +# with_dict: "{{ category.current_audit_policy }}" + +#################### +### apply change ### +#################### + +- name: enable category + win_audit_policy_system: + category: "{{ category_name }}" + audit_type: success + register: category + +- name: enable subcategory + win_audit_policy_system: + subcategory: "{{ subcategory_name }}" + audit_type: success, failure + register: subcategory + +- name: enable assert that changed is true + assert: + that: + - category is changed + - subcategory is changed + +- name: enable assert that audit_type is "success" for category + assert: + that: + - item == "success" + with_items: + - "{{ category.current_audit_policy.values() | list | unique }}" + +- name: enable assert that audit_type is "success and failure" for subcategory + assert: + that: + - item == "success and failure" + with_items: + - "{{ subcategory.current_audit_policy.values() | list }}" + +############################### +### idempotent apply change ### +############################### + +- name: idem enable category + win_audit_policy_system: + category: "{{ category_name }}" + audit_type: success + register: category + +- name: idem enable subcategory + win_audit_policy_system: + subcategory: "{{ subcategory_name }}" + audit_type: success, failure + register: subcategory + +- name: idem assert that changed is false + assert: + that: + - category is not changed + - subcategory is not changed + +- name: idem assert that audit_type is "success" for category + assert: + that: + - item == "success" + with_items: + - "{{ category.current_audit_policy.values() | list | unique }}" + +- name: idem assert that audit_type is "success and failure" for subcategory + assert: + that: + - item == "success and failure" + with_items: + - "{{ subcategory.current_audit_policy.values() | list }}" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_audit_policy_system/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_audit_policy_system/tasks/main.yml new file mode 100644 index 000000000..c2e55accf --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_audit_policy_system/tasks/main.yml @@ -0,0 +1,25 @@ +#turn off so then we can test changes occur on enable. Turning off for object access also +#covers our subcategory test for file system +- name: turn off auditing for category + win_audit_policy_system: + category: "{{ category_name }}" + audit_type: none + +- name: turn off auditing for subcategory + win_audit_policy_system: + subcategory: "{{ subcategory_name }}" + audit_type: none + +- block: + - include_tasks: add.yml + - include_tasks: remove.yml + always: + - name: CLEANUP turn "{{ category_name }}" back to no auditing + win_audit_policy_system: + category: "{{ category_name }}" + audit_type: none + + - name: CLEANUP turn "{{ subcategory_name }}" back to no auditing + win_audit_policy_system: + subcategory: "{{ subcategory_name }}" + audit_type: none diff --git a/ansible_collections/community/windows/tests/integration/targets/win_audit_policy_system/tasks/remove.yml b/ansible_collections/community/windows/tests/integration/targets/win_audit_policy_system/tasks/remove.yml new file mode 100644 index 000000000..1cd60b0ab --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_audit_policy_system/tasks/remove.yml @@ -0,0 +1,96 @@ +######################### +### check mode remove ### +######################### +- name: check mode disable category + win_audit_policy_system: + category: "{{ category_name }}" + audit_type: none + check_mode: yes + register: category + +- name: check mode disable subcategory + win_audit_policy_system: + subcategory: "{{ subcategory_name }}" + audit_type: none + check_mode: yes + register: subcategory + +- name: check mode assert that changed is true + assert: + that: + - category is changed + - subcategory is changed + +- name: check mode assert that audit_type is still "success" (old value) for category + assert: + that: + - item == "success" + with_items: + - "{{ category.current_audit_policy.values() | list | unique }}" + +- name: check mode assert that audit_type is still "success and failure" (old value) for subcategory + assert: + that: + - item == "success and failure" + with_items: + - "{{ subcategory.current_audit_policy.values() | list }}" + +###################### +### disable policy ### +###################### + +- name: disable category + win_audit_policy_system: + category: "{{ category_name }}" + audit_type: none + register: category + +- name: disable subcategory + win_audit_policy_system: + subcategory: "{{ subcategory_name }}" + audit_type: none + register: subcategory + +- name: assert that changed is true + assert: + that: + - category is changed + - subcategory is changed + +- name: assert that audit_type is "no auditing" + assert: + that: + - item == "no auditing" + with_items: + - "{{ subcategory.current_audit_policy.values() | list }}" + - "{{ category.current_audit_policy.values() | list | unique }}" + +########################## +### idempotent disable ### +########################## + +- name: idem disable category + win_audit_policy_system: + category: "{{ category_name }}" + audit_type: none + register: category + +- name: idem disable subcategory + win_audit_policy_system: + subcategory: "{{ subcategory_name }}" + audit_type: none + register: subcategory + +- name: idem assert that changed is false + assert: + that: + - category is not changed + - subcategory is not changed + +- name: assert that audit_type is "no auditing" + assert: + that: + - item == "no auditing" + with_items: + - "{{ subcategory.current_audit_policy.values() | list }}" + - "{{ category.current_audit_policy.values() | list | unique }}" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_audit_rule/aliases b/ansible_collections/community/windows/tests/integration/targets/win_audit_rule/aliases new file mode 100644 index 000000000..423ce3910 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_audit_rule/aliases @@ -0,0 +1 @@ +shippable/windows/group2 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_audit_rule/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_audit_rule/defaults/main.yml new file mode 100644 index 000000000..f0faa9a56 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_audit_rule/defaults/main.yml @@ -0,0 +1,7 @@ +test_audit_rule_folder: c:\windows\temp\{{ 'ansible test win_audit_policy' | to_uuid }} +test_audit_rule_file: c:\windows\temp\{{ 'ansible test win_audit_policy' | to_uuid }}.txt +test_audit_rule_registry: HKCU:\{{ 'ansible test win_audit_policy' | to_uuid }} +test_audit_rule_rights: 'delete' +test_audit_rule_new_rights: 'delete,changepermissions' +test_audit_rule_user: 'everyone' +test_audit_rule_audit_flags: success diff --git a/ansible_collections/community/windows/tests/integration/targets/win_audit_rule/library/test_get_audit_rule.ps1 b/ansible_collections/community/windows/tests/integration/targets/win_audit_rule/library/test_get_audit_rule.ps1 new file mode 100644 index 000000000..37096c21a --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_audit_rule/library/test_get_audit_rule.ps1 @@ -0,0 +1,93 @@ +#!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 + +$params = Parse-Args -arguments $args -supports_check_mode $true + +# these are your module parameters +$path = Get-AnsibleParam -obj $params -name "path" -type "path" -failifempty $true -aliases "destination", "dest" +$user = Get-AnsibleParam -obj $params -name "user" -type "str" -failifempty $true +$rights = Get-AnsibleParam -obj $params -name "rights" -type "list" +$inheritance_flags = Get-AnsibleParam -obj $params -name "inheritance_flags" -type "list" -default 'ContainerInherit', 'ObjectInherit' +$propOptions = 'InheritOnly', 'None', 'NoPropagateInherit' +$propagation_flags = Get-AnsibleParam -obj $params -name "propagation_flags" -type "str" -default "none" -ValidateSet $propOptions +$audit_flags = Get-AnsibleParam -obj $params -name "audit_flags" -type "list" -default "success" #-ValidateSet 'Success','Failure' +#$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset 'present','absent' + + +If (! (Test-Path $path) ) { + Fail-Json $result "Path not found ($path)" +} + +Function Get-CurrentAuditRule ($path) { + $ACL = Get-Acl -Path $path -Audit + + $HT = Foreach ($Obj in $ACL.Audit) { + @{ + user = $Obj.IdentityReference.ToString() + rights = ($Obj | Select-Object -expand "*rights").ToString() + audit_flags = $Obj.AuditFlags.ToString() + is_inherited = $Obj.InheritanceFlags.ToString() + inheritance_flags = $Obj.IsInherited.ToString() + propagation_flags = $Obj.PropagationFlags.ToString() + } + } + + If (-Not $HT) { + "No audit rules defined on $path" + } + Else { $HT } +} + + +$result = @{ + changed = $false + matching_rule_found = $false + current_audit_rules = Get-CurrentAuditRule $path +} + +$ACL = Get-ACL $Path -Audit +$SID = Convert-ToSid $user + +$ItemType = (Get-Item $path).GetType() +switch ($ItemType) { + ([Microsoft.Win32.RegistryKey]) { + $rights = [System.Security.AccessControl.RegistryRights]$rights + $result.path_type = 'registry' + } + ([System.IO.FileInfo]) { + $rights = [System.Security.AccessControl.FileSystemRights]$rights + $result.path_type = 'file' + } + ([System.IO.DirectoryInfo]) { + $rights = [System.Security.AccessControl.FileSystemRights]$rights + $result.path_type = 'directory' + } +} + +$flags = [System.Security.AccessControl.AuditFlags]$audit_flags +$inherit = [System.Security.AccessControl.InheritanceFlags]$inheritance_flags +$prop = [System.Security.AccessControl.PropagationFlags]$propagation_flags + +Foreach ($group in $ACL.Audit) { + #exit here if any existing rule matches defined rule, otherwise exit below + #with no matches + If ( + ($group | Select-Object -expand "*Rights") -eq $rights -and + $group.AuditFlags -eq $flags -and + $group.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) -eq $SID -and + $group.InheritanceFlags -eq $inherit -and + $group.PropagationFlags -eq $prop + ) { + $result.matching_rule_found = $true + $result.current_audit_rules = Get-CurrentAuditRule $path + Exit-Json $result + } +} + +$result.current_audit_rules = Get-CurrentAuditRule $path +Exit-Json $result diff --git a/ansible_collections/community/windows/tests/integration/targets/win_audit_rule/tasks/add.yml b/ansible_collections/community/windows/tests/integration/targets/win_audit_rule/tasks/add.yml new file mode 100644 index 000000000..2a059a88c --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_audit_rule/tasks/add.yml @@ -0,0 +1,172 @@ +###################### +### check mode add ### +###################### +- name: check mode ADD audit policy directory + win_audit_rule: + path: "{{ test_audit_rule_folder }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_rights }}" + state: present + audit_flags: "{{ test_audit_rule_audit_flags }}" + register: directory + check_mode: yes + +- name: check mode ADD audit policy file + win_audit_rule: + path: "{{ test_audit_rule_file }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_rights }}" + state: present + audit_flags: "{{ test_audit_rule_audit_flags }}" + inheritance_flags: none + register: file + check_mode: yes + +- name: check mode ADD audit policy registry + win_audit_rule: + path: "{{ test_audit_rule_registry }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_rights }}" + state: present + audit_flags: "{{ test_audit_rule_audit_flags }}" + register: registry + check_mode: yes + +- name: check mode ADD get directory results + test_get_audit_rule: + path: "{{ test_audit_rule_folder }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_rights }}" + audit_flags: "{{ test_audit_rule_audit_flags }}" + register: directory_results + +- name: check mode ADD get file results + test_get_audit_rule: + path: "{{ test_audit_rule_file }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_rights }}" + audit_flags: "{{ test_audit_rule_audit_flags }}" + inheritance_flags: none + register: file_results + +- name: check mode ADD get REGISTRY results + test_get_audit_rule: + path: "{{ test_audit_rule_registry }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_rights }}" + audit_flags: "{{ test_audit_rule_audit_flags }}" + register: registry_results + +- name: check mode ADD assert that a change is needed, but no change occurred to the audit rules + assert: + that: + - directory is changed + - file is changed + - registry is changed + - not directory_results.matching_rule_found and directory_results.path_type == 'directory' + - not file_results.matching_rule_found and file_results.path_type == 'file' + - not registry_results.matching_rule_found and registry_results.path_type == 'registry' + +################## +### add a rule ### +################## +- name: ADD audit policy directory + win_audit_rule: + path: "{{ test_audit_rule_folder }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_rights }}" + state: present + audit_flags: "{{ test_audit_rule_audit_flags }}" + register: directory + +- name: ADD audit policy file + win_audit_rule: + path: "{{ test_audit_rule_file }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_rights }}" + state: present + audit_flags: "{{ test_audit_rule_audit_flags }}" + inheritance_flags: none + register: file + +- name: ADD audit policy registry + win_audit_rule: + path: "{{ test_audit_rule_registry }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_rights }}" + state: present + audit_flags: "{{ test_audit_rule_audit_flags }}" + register: registry + +- name: ADD get directory results + test_get_audit_rule: + path: "{{ test_audit_rule_folder }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_rights }}" + audit_flags: "{{ test_audit_rule_audit_flags }}" + register: directory_results + +- name: ADD get file results + test_get_audit_rule: + path: "{{ test_audit_rule_file }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_rights }}" + audit_flags: "{{ test_audit_rule_audit_flags }}" + inheritance_flags: none + register: file_results + +- name: ADD get REGISTRY results + test_get_audit_rule: + path: "{{ test_audit_rule_registry }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_rights }}" + audit_flags: "{{ test_audit_rule_audit_flags }}" + register: registry_results + +- name: ADD assert that the rules were added and a change is detected + assert: + that: + - directory is changed + - file is changed + - registry is changed + - directory_results.matching_rule_found and directory_results.path_type == 'directory' + - file_results.matching_rule_found and file_results.path_type == 'file' + - registry_results.matching_rule_found and registry_results.path_type == 'registry' + +############################# +### idempotent add a rule ### +############################# +- name: idempotent ADD audit policy directory + win_audit_rule: + path: "{{ test_audit_rule_folder }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_rights }}" + state: present + audit_flags: "{{ test_audit_rule_audit_flags }}" + register: directory + +- name: idempotent ADD audit policy file + win_audit_rule: + path: "{{ test_audit_rule_file }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_rights }}" + state: present + audit_flags: "{{ test_audit_rule_audit_flags }}" + inheritance_flags: none + register: file + +- name: idempotent ADD audit policy registry idempotent + win_audit_rule: + path: "{{ test_audit_rule_registry }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_rights }}" + state: present + audit_flags: "{{ test_audit_rule_audit_flags }}" + register: registry + +- name: idempotent ADD assert that a change did not occur + assert: + that: + - directory is not changed and directory.path_type == 'directory' + - file is not changed and file.path_type == 'file' + - registry is not changed and registry.path_type == 'registry' diff --git a/ansible_collections/community/windows/tests/integration/targets/win_audit_rule/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_audit_rule/tasks/main.yml new file mode 100644 index 000000000..cdeff7a3a --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_audit_rule/tasks/main.yml @@ -0,0 +1,33 @@ +- name: create temporary folder to test with + ansible.windows.win_file: + path: "{{ test_audit_rule_folder }}" + state: directory + +- name: create temporary file to test with + ansible.windows.win_file: + path: "{{ test_audit_rule_file }}" + state: touch + +- name: create temporary registry key to test with + ansible.windows.win_regedit: + path: "{{ test_audit_rule_registry }}" + +- block: + - include_tasks: add.yml + - include_tasks: modify.yml + - include_tasks: remove.yml + always: + - name: remove testing folder + ansible.windows.win_file: + path: "{{ test_audit_rule_folder }}" + state: absent + + - name: remove testing file + ansible.windows.win_file: + path: "{{ test_audit_rule_file }}" + state: absent + + - name: remove registry key + ansible.windows.win_regedit: + path: "{{ test_audit_rule_registry }}" + state: absent diff --git a/ansible_collections/community/windows/tests/integration/targets/win_audit_rule/tasks/modify.yml b/ansible_collections/community/windows/tests/integration/targets/win_audit_rule/tasks/modify.yml new file mode 100644 index 000000000..1db07e2b4 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_audit_rule/tasks/modify.yml @@ -0,0 +1,172 @@ +######################### +### modify check mode ### +######################### +- name: check mode modify audit policy directory + win_audit_rule: + path: "{{ test_audit_rule_folder }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_new_rights }}" + state: present + audit_flags: "{{ test_audit_rule_audit_flags }}" + register: directory + check_mode: yes + +- name: check mode modify audit policy file + win_audit_rule: + path: "{{ test_audit_rule_file }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_new_rights }}" + state: present + audit_flags: "{{ test_audit_rule_audit_flags }}" + inheritance_flags: none + register: file + check_mode: yes + +- name: check mode modify audit policy registry + win_audit_rule: + path: "{{ test_audit_rule_registry }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_new_rights }}" + state: present + audit_flags: "{{ test_audit_rule_audit_flags }}" + register: registry + check_mode: yes + +- name: check mode modify get directory rule results + test_get_audit_rule: + path: "{{ test_audit_rule_folder }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_new_rights }}" + audit_flags: "{{ test_audit_rule_audit_flags }}" + register: directory_results + +- name: check mode modify get file rule results + test_get_audit_rule: + path: "{{ test_audit_rule_file }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_new_rights }}" + audit_flags: "{{ test_audit_rule_audit_flags }}" + inheritance_flags: none + register: file_results + +- name: check mode modify get REGISTRY rule results + test_get_audit_rule: + path: "{{ test_audit_rule_registry }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_new_rights }}" + audit_flags: "{{ test_audit_rule_audit_flags }}" + register: registry_results + +- name: check mode modify assert that change is needed but rights still equal the original rights and not test_audit_rule_new_rights + assert: + that: + - directory is changed + - file is changed + - registry is changed + - not directory_results.matching_rule_found and directory_results.path_type == 'directory' + - not file_results.matching_rule_found and file_results.path_type == 'file' + - not registry_results.matching_rule_found and registry_results.path_type == 'registry' + +############## +### modify ### +############## +- name: modify audit policy directory + win_audit_rule: + path: "{{ test_audit_rule_folder }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_new_rights }}" + state: present + audit_flags: "{{ test_audit_rule_audit_flags }}" + register: directory + +- name: modify audit policy file + win_audit_rule: + path: "{{ test_audit_rule_file }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_new_rights }}" + state: present + audit_flags: "{{ test_audit_rule_audit_flags }}" + inheritance_flags: none + register: file + +- name: modify audit policy registry + win_audit_rule: + path: "{{ test_audit_rule_registry }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_new_rights }}" + state: present + audit_flags: "{{ test_audit_rule_audit_flags }}" + register: registry + +- name: modify get directory rule results + test_get_audit_rule: + path: "{{ test_audit_rule_folder }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_new_rights }}" + audit_flags: "{{ test_audit_rule_audit_flags }}" + register: directory_results + +- name: modify get file rule results + test_get_audit_rule: + path: "{{ test_audit_rule_file }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_new_rights }}" + audit_flags: "{{ test_audit_rule_audit_flags }}" + inheritance_flags: none + register: file_results + +- name: modify get REGISTRY rule results + test_get_audit_rule: + path: "{{ test_audit_rule_registry }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_new_rights }}" + audit_flags: "{{ test_audit_rule_audit_flags }}" + register: registry_results + +- name: modify assert that the rules were modified and a change is detected + assert: + that: + - directory is changed + - file is changed + - registry is changed + - directory_results.matching_rule_found and directory_results.path_type == 'directory' + - file_results.matching_rule_found and file_results.path_type == 'file' + - registry_results.matching_rule_found and registry_results.path_type == 'registry' + +##################################### +### idempotent test modify a rule ### +##################################### +- name: idempotent modify audit policy directory + win_audit_rule: + path: "{{ test_audit_rule_folder }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_new_rights }}" + state: present + audit_flags: "{{ test_audit_rule_audit_flags }}" + register: directory + +- name: idempotent modify audit policy file + win_audit_rule: + path: "{{ test_audit_rule_file }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_new_rights }}" + state: present + audit_flags: "{{ test_audit_rule_audit_flags }}" + inheritance_flags: none + register: file + +- name: idempotent modify audit policy registry + win_audit_rule: + path: "{{ test_audit_rule_registry }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_new_rights }}" + state: present + audit_flags: "{{ test_audit_rule_audit_flags }}" + register: registry + +- name: idempotent modify assert that and a change is not detected + assert: + that: + - directory is not changed and directory.path_type == 'directory' + - file is not changed and file.path_type == 'file' + - registry is not changed and registry.path_type == 'registry' diff --git a/ansible_collections/community/windows/tests/integration/targets/win_audit_rule/tasks/remove.yml b/ansible_collections/community/windows/tests/integration/targets/win_audit_rule/tasks/remove.yml new file mode 100644 index 000000000..3102bc748 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_audit_rule/tasks/remove.yml @@ -0,0 +1,151 @@ +################################ +### check mode remove a rule ### +################################ +- name: check mode remove directory rule + win_audit_rule: + path: "{{ test_audit_rule_folder }}" + user: "{{ test_audit_rule_user }}" + state: absent + register: directory + check_mode: yes + +- name: check mode remove file rule + win_audit_rule: + path: "{{ test_audit_rule_file }}" + user: "{{ test_audit_rule_user }}" + state: absent + register: file + check_mode: yes + +- name: check mode remove registry rule + win_audit_rule: + path: "{{ test_audit_rule_registry }}" + user: "{{ test_audit_rule_user }}" + state: absent + register: registry + check_mode: yes + +- name: check mode remove get directory rule results + test_get_audit_rule: + path: "{{ test_audit_rule_folder }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_new_rights }}" + audit_flags: "{{ test_audit_rule_audit_flags }}" + register: directory_results + +- name: check mode remove get file rule results + test_get_audit_rule: + path: "{{ test_audit_rule_file }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_new_rights }}" + audit_flags: "{{ test_audit_rule_audit_flags }}" + inheritance_flags: none + register: file_results + +- name: check mode remove get REGISTRY rule results + test_get_audit_rule: + path: "{{ test_audit_rule_registry }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_new_rights }}" + audit_flags: "{{ test_audit_rule_audit_flags }}" + register: registry_results + +- name: check mode remove assert that change detected, but rule is still present + assert: + that: + - directory is changed + - file is changed + - registry is changed + - directory_results.matching_rule_found and directory_results.path_type == 'directory' + - file_results.matching_rule_found and file_results.path_type == 'file' + - registry_results.matching_rule_found and registry_results.path_type == 'registry' + +##################### +### remove a rule ### +##################### +- name: remove directory rule + win_audit_rule: + path: "{{ test_audit_rule_folder }}" + user: "{{ test_audit_rule_user }}" + state: absent + register: directory + +- name: remove file rule + win_audit_rule: + path: "{{ test_audit_rule_file }}" + user: "{{ test_audit_rule_user }}" + state: absent + register: file + +- name: remove registry rule + win_audit_rule: + path: "{{ test_audit_rule_registry }}" + user: "{{ test_audit_rule_user }}" + state: absent + register: registry + +- name: remove get directory rule results + test_get_audit_rule: + path: "{{ test_audit_rule_folder }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_new_rights }}" + audit_flags: "{{ test_audit_rule_audit_flags }}" + register: directory_results + +- name: remove get file rule results + test_get_audit_rule: + path: "{{ test_audit_rule_file }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_new_rights }}" + audit_flags: "{{ test_audit_rule_audit_flags }}" + inheritance_flags: none + register: file_results + +- name: remove get REGISTRY rule results + test_get_audit_rule: + path: "{{ test_audit_rule_registry }}" + user: "{{ test_audit_rule_user }}" + rights: "{{ test_audit_rule_new_rights }}" + audit_flags: "{{ test_audit_rule_audit_flags }}" + register: registry_results + +- name: remove assert that change detected and rule is gone + assert: + that: + - directory is changed + - file is changed + - registry is changed + - not directory_results.matching_rule_found and directory_results.path_type == 'directory' + - not file_results.matching_rule_found and file_results.path_type == 'file' + - not registry_results.matching_rule_found and registry_results.path_type == 'registry' + +################################ +### idempotent remove a rule ### +################################ +- name: idempotent remove directory rule + win_audit_rule: + path: "{{ test_audit_rule_folder }}" + user: "{{ test_audit_rule_user }}" + state: absent + register: directory + +- name: idempotent remove file rule + win_audit_rule: + path: "{{ test_audit_rule_file }}" + user: "{{ test_audit_rule_user }}" + state: absent + register: file + +- name: idempotent remove registry rule + win_audit_rule: + path: "{{ test_audit_rule_registry }}" + user: "{{ test_audit_rule_user }}" + state: absent + register: registry + +- name: idempotent remove assert that no change detected + assert: + that: + - directory is not changed and directory.path_type == 'directory' + - file is not changed and file.path_type == 'file' + - registry is not changed and registry.path_type == 'registry' diff --git a/ansible_collections/community/windows/tests/integration/targets/win_auto_logon/aliases b/ansible_collections/community/windows/tests/integration/targets/win_auto_logon/aliases new file mode 100644 index 000000000..215e0b069 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_auto_logon/aliases @@ -0,0 +1 @@ +shippable/windows/group4 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_auto_logon/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_auto_logon/defaults/main.yml new file mode 100644 index 000000000..d5462bb6a --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_auto_logon/defaults/main.yml @@ -0,0 +1,3 @@ +# This doesn't have to be valid, just testing weird chars in the pass +test_logon_password: 'café - 💩' +test_logon_password2: '.ÅÑŚÌβŁÈ [$!@^&test(;)]' diff --git a/ansible_collections/community/windows/tests/integration/targets/win_auto_logon/library/test_autologon_info.ps1 b/ansible_collections/community/windows/tests/integration/targets/win_auto_logon/library/test_autologon_info.ps1 new file mode 100644 index 000000000..2819151ff --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_auto_logon/library/test_autologon_info.ps1 @@ -0,0 +1,215 @@ +#!powershell + +#AnsibleRequires -CSharpUtil Ansible.Basic +#Requires -Module Ansible.ModuleUtils.AddType + +$module = [Ansible.Basic.AnsibleModule]::Create($args, @{}) + +Add-CSharpType -AnsibleModule $module -References @' +using Microsoft.Win32.SafeHandles; +using System; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ansible.TestAutoLogonInfo +{ + internal class NativeHelpers + { + [StructLayout(LayoutKind.Sequential)] + public class LSA_OBJECT_ATTRIBUTES + { + public UInt32 Length = 0; + public IntPtr RootDirectory = IntPtr.Zero; + public IntPtr ObjectName = IntPtr.Zero; + public UInt32 Attributes = 0; + public IntPtr SecurityDescriptor = IntPtr.Zero; + public IntPtr SecurityQualityOfService = IntPtr.Zero; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct LSA_UNICODE_STRING + { + public UInt16 Length; + public UInt16 MaximumLength; + public IntPtr Buffer; + + public static explicit operator string(LSA_UNICODE_STRING s) + { + byte[] strBytes = new byte[s.Length]; + Marshal.Copy(s.Buffer, strBytes, 0, s.Length); + return Encoding.Unicode.GetString(strBytes); + } + + public static SafeMemoryBuffer CreateSafeBuffer(string s) + { + if (s == null) + return new SafeMemoryBuffer(IntPtr.Zero); + + byte[] stringBytes = Encoding.Unicode.GetBytes(s); + int structSize = Marshal.SizeOf(typeof(LSA_UNICODE_STRING)); + IntPtr buffer = Marshal.AllocHGlobal(structSize + stringBytes.Length); + try + { + LSA_UNICODE_STRING lsaString = new LSA_UNICODE_STRING() + { + Length = (UInt16)(stringBytes.Length), + MaximumLength = (UInt16)(stringBytes.Length), + Buffer = IntPtr.Add(buffer, structSize), + }; + Marshal.StructureToPtr(lsaString, buffer, false); + Marshal.Copy(stringBytes, 0, lsaString.Buffer, stringBytes.Length); + return new SafeMemoryBuffer(buffer); + } + catch + { + // Make sure we free the pointer before raising the exception. + Marshal.FreeHGlobal(buffer); + throw; + } + } + } + } + + internal class NativeMethods + { + [DllImport("Advapi32.dll")] + public static extern UInt32 LsaClose( + IntPtr ObjectHandle); + + [DllImport("Advapi32.dll")] + public static extern UInt32 LsaFreeMemory( + IntPtr Buffer); + + [DllImport("Advapi32.dll")] + internal static extern Int32 LsaNtStatusToWinError( + UInt32 Status); + + [DllImport("Advapi32.dll")] + public static extern UInt32 LsaOpenPolicy( + IntPtr SystemName, + NativeHelpers.LSA_OBJECT_ATTRIBUTES ObjectAttributes, + UInt32 AccessMask, + out SafeLsaHandle PolicyHandle); + + [DllImport("Advapi32.dll")] + public static extern UInt32 LsaRetrievePrivateData( + SafeLsaHandle PolicyHandle, + SafeMemoryBuffer KeyName, + out SafeLsaMemory PrivateData); + } + + internal class SafeLsaMemory : SafeBuffer + { + internal SafeLsaMemory() : base(true) { } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + + protected override bool ReleaseHandle() + { + return NativeMethods.LsaFreeMemory(handle) == 0; + } + } + + internal class SafeMemoryBuffer : SafeBuffer + { + internal SafeMemoryBuffer() : base(true) { } + + internal SafeMemoryBuffer(IntPtr ptr) : base(true) + { + base.SetHandle(ptr); + } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + + protected override bool ReleaseHandle() + { + if (handle != IntPtr.Zero) + Marshal.FreeHGlobal(handle); + return true; + } + } + + public class SafeLsaHandle : SafeHandleZeroOrMinusOneIsInvalid + { + internal SafeLsaHandle() : base(true) { } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + + protected override bool ReleaseHandle() + { + return NativeMethods.LsaClose(handle) == 0; + } + } + + public class Win32Exception : System.ComponentModel.Win32Exception + { + private string _exception_msg; + public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { } + public Win32Exception(int errorCode, string message) : base(errorCode) + { + _exception_msg = String.Format("{0} - {1} (Win32 Error Code {2}: 0x{3})", message, base.Message, errorCode, errorCode.ToString("X8")); + } + public override string Message { get { return _exception_msg; } } + public static explicit operator Win32Exception(string message) { return new Win32Exception(message); } + } + + public class LsaUtil + { + public static SafeLsaHandle OpenPolicy(UInt32 access) + { + NativeHelpers.LSA_OBJECT_ATTRIBUTES oa = new NativeHelpers.LSA_OBJECT_ATTRIBUTES(); + SafeLsaHandle lsaHandle; + UInt32 res = NativeMethods.LsaOpenPolicy(IntPtr.Zero, oa, access, out lsaHandle); + if (res != 0) + throw new Win32Exception(NativeMethods.LsaNtStatusToWinError(res), + String.Format("LsaOpenPolicy({0}) failed", access.ToString())); + return lsaHandle; + } + + public static string RetrievePrivateData(SafeLsaHandle handle, string key) + { + using (SafeMemoryBuffer keyBuffer = NativeHelpers.LSA_UNICODE_STRING.CreateSafeBuffer(key)) + { + SafeLsaMemory buffer; + UInt32 res = NativeMethods.LsaRetrievePrivateData(handle, keyBuffer, out buffer); + using (buffer) + { + if (res != 0) + { + // If the data object was not found we return null to indicate it isn't set. + if (res == 0xC0000034) // STATUS_OBJECT_NAME_NOT_FOUND + return null; + + throw new Win32Exception(NativeMethods.LsaNtStatusToWinError(res), + String.Format("LsaRetrievePrivateData({0}) failed", key)); + } + + NativeHelpers.LSA_UNICODE_STRING lsaString = (NativeHelpers.LSA_UNICODE_STRING) + Marshal.PtrToStructure(buffer.DangerousGetHandle(), + typeof(NativeHelpers.LSA_UNICODE_STRING)); + return (string)lsaString; + } + } + } + } +} +'@ + +$details = Get-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' +$module.Result.AutoAdminLogon = $details.AutoAdminLogon +$module.Result.DefaultUserName = $details.DefaultUserName +$module.Result.DefaultDomainName = $details.DefaultDomainName +$module.Result.DefaultPassword = $details.DefaultPassword +$module.Result.AutoLogonCount = $details.AutoLogonCount + +$handle = [Ansible.TestAutoLogonInfo.LsaUtil]::OpenPolicy(0x00000004) +try { + $password = [Ansible.TestAutoLogonInfo.LsaUtil]::RetrievePrivateData($handle, 'DefaultPassword') + $module.Result.LsaPassword = $password +} +finally { + $handle.Dispose() +} + +$module.ExitJson() diff --git a/ansible_collections/community/windows/tests/integration/targets/win_auto_logon/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_auto_logon/tasks/main.yml new file mode 100644 index 000000000..d99649e3e --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_auto_logon/tasks/main.yml @@ -0,0 +1,42 @@ +--- +- name: get user domain split for ansible_user + ansible.windows.win_shell: | + $account = New-Object -TypeName System.Security.Principal.NTAccount -ArgumentList '{{ ansible_user }}' + $sid = $account.Translate([System.Security.Principal.SecurityIdentifier]) + $sid.Translate([System.Security.Principal.NTAccount]).Value -split '{{ "\\" }}' + changed_when: False + register: test_user_split + +- set_fact: + test_domain: '{{ test_user_split.stdout_lines[0] }}' + test_user: '{{ test_user_split.stdout_lines[1] }}' + +- name: ensure auto logon is cleared before test + win_auto_logon: + state: absent + +- name: ensure defaults are set + ansible.windows.win_regedit: + path: HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon + name: '{{ item.name }}' + data: '{{ item.value }}' + type: '{{ item.type }}' + state: present + loop: + # We set the DefaultPassword to ensure win_auto_logon clears this out + - name: DefaultPassword + value: abc + type: string + # Ensures the host we test on has a baseline key to check against + - name: AutoAdminLogon + value: 0 + type: dword + +- block: + - name: run tests + include_tasks: tests.yml + + always: + - name: make sure the auto logon is cleared + win_auto_logon: + state: absent diff --git a/ansible_collections/community/windows/tests/integration/targets/win_auto_logon/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_auto_logon/tasks/tests.yml new file mode 100644 index 000000000..c25e07709 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_auto_logon/tasks/tests.yml @@ -0,0 +1,178 @@ +# Copyright: (c) 2019, Prasoon Karunan V (@prasoonkarunan) <kvprasoon@Live.in> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: set autologon registry keys (check mode) + win_auto_logon: + username: '{{ ansible_user }}' + password: '{{ test_logon_password }}' + state: present + register: set_check + check_mode: yes + +- name: get acutal of set autologon registry keys (check mode) + test_autologon_info: + register: set_actual_check + +- name: assert set autologon registry keys (check mode) + assert: + that: + - set_check is changed + - set_actual_check.AutoAdminLogon == 0 + - set_actual_check.AutoLogonCount == None + - set_actual_check.DefaultDomainName == None + - set_actual_check.DefaultPassword == 'abc' + - set_actual_check.DefaultUserName == None + - set_actual_check.LsaPassword == None + +- name: set autologon registry keys + win_auto_logon: + username: '{{ ansible_user }}' + password: '{{ test_logon_password }}' + state: present + register: set + +- name: get acutal of set autologon registry keys + test_autologon_info: + register: set_actual + +- name: assert set autologon registry keys + assert: + that: + - set is changed + - set_actual.AutoAdminLogon == 1 + - set_actual.AutoLogonCount == None + - set_actual.DefaultDomainName == test_domain + - set_actual.DefaultPassword == None + - set_actual.DefaultUserName == test_user + - set_actual.LsaPassword == test_logon_password + +- name: set autologon registry keys (idempotent) + win_auto_logon: + username: '{{ ansible_user }}' + password: '{{ test_logon_password }}' + state: present + register: set_again + +- name: assert set autologon registry keys (idempotent) + assert: + that: + - not set_again is changed + +- name: add logon count (check mode) + win_auto_logon: + username: '{{ ansible_user }}' + password: '{{ test_logon_password }}' + logon_count: 2 + state: present + register: logon_count_check + check_mode: yes + +- name: get result of add logon count (check mode) + test_autologon_info: + register: logon_count_actual_check + +- name: assert add logon count (check mode) + assert: + that: + - logon_count_check is changed + - logon_count_actual_check.AutoLogonCount == None + +- name: add logon count + win_auto_logon: + username: '{{ ansible_user }}' + password: '{{ test_logon_password }}' + logon_count: 2 + state: present + register: logon_count + +- name: get result of add logon count + test_autologon_info: + register: logon_count_actual + +- name: assert add logon count + assert: + that: + - logon_count is changed + - logon_count_actual.AutoLogonCount == 2 + +- name: change auto logon (check mode) + win_auto_logon: + username: '{{ ansible_user }}' + password: '{{ test_logon_password2 }}' + state: present + register: change_check + check_mode: yes + +- name: get reuslt of change auto logon (check mode) + test_autologon_info: + register: change_actual_check + +- name: assert change auto logon (check mode) + assert: + that: + - change_check is changed + - change_actual_check == logon_count_actual + +- name: change auto logon + win_auto_logon: + username: '{{ ansible_user }}' + password: '{{ test_logon_password2 }}' + state: present + register: change + +- name: get reuslt of change auto logon + test_autologon_info: + register: change_actual + +- name: assert change auto logon + assert: + that: + - change is changed + - change_actual.AutoLogonCount == None + - change_actual.LsaPassword == test_logon_password2 + +- name: remove autologon registry keys (check mode) + win_auto_logon: + state: absent + register: remove_check + check_mode: yes + +- name: get result of remove autologon registry keys (check mode) + test_autologon_info: + register: remove_actual_check + +- name: assert remove autologon registry keys (check mode) + assert: + that: + - remove_check is changed + - remove_actual_check == change_actual + +- name: remove autologon registry keys + win_auto_logon: + state: absent + register: remove + +- name: get result of remove autologon registry keys + test_autologon_info: + register: remove_actual + +- name: assert remove autologon registry keys + assert: + that: + - remove is changed + - remove_actual.AutoAdminLogon == 0 + - remove_actual.AutoLogonCount == None + - remove_actual.DefaultDomainName == None + - remove_actual.DefaultPassword == None + - remove_actual.DefaultUserName == None + - remove_actual.LsaPassword == None + +- name: remove autologon registry keys (idempotent) + win_auto_logon: + state: absent + register: remove_again + +- name: assert remove autologon registry keys (idempotent) + assert: + that: + - not remove_again is changed diff --git a/ansible_collections/community/windows/tests/integration/targets/win_certificate_info/aliases b/ansible_collections/community/windows/tests/integration/targets/win_certificate_info/aliases new file mode 100644 index 000000000..3cf5b97e8 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_certificate_info/aliases @@ -0,0 +1 @@ +shippable/windows/group3 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_certificate_info/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_certificate_info/defaults/main.yml new file mode 100644 index 000000000..871dfe91d --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_certificate_info/defaults/main.yml @@ -0,0 +1,3 @@ +win_cert_dir: '{{ remote_tmp_dir }}\win_certificate .ÅÑŚÌβŁÈ [$!@^&test(;)]' +subj_thumbprint: 'BD7AF104CF1872BDB518D95C9534EA941665FD27' +root_thumbprint: 'BC05633694E675449136679A658281F17A191087' diff --git a/ansible_collections/community/windows/tests/integration/targets/win_certificate_info/files/root-cert.pem b/ansible_collections/community/windows/tests/integration/targets/win_certificate_info/files/root-cert.pem new file mode 100644 index 000000000..edbe6b868 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_certificate_info/files/root-cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDKDCCAhCgAwIBAgIJAP1vIdGgMJv/MA0GCSqGSIb3DQEBCwUAMCgxGTAXBgNV +BAMMEHJvb3QuYW5zaWJsZS5jb20xCzAJBgNVBAYTAlVTMCAXDTE3MTIxNTA4Mzkz +MloYDzIwODYwMTAyMDgzOTMyWjAoMRkwFwYDVQQDDBByb290LmFuc2libGUuY29t +MQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMmq +YT8eZY6rFQKnmScUGnnUH1tLQ+3WQpfKiWygCUSb1CNqO3J1u3pGMEqYM58LK4Kr +Mpskv7K1tCV/EMZqGTqXAIfSLy9umlb/9C3AhL9thBPn5I9dam/EmrIZktI9/w5Y +wBXn4toe+OopA3QkMQh9BUjUCPb9fdOI+ir7OGFZMmxXmiM64+BEeywM2oSGsdZ9 +5hU378UBu2IX4+OAV8Fbr2l6VW+Fxg/tKIOo6Bs46Pa4EZgtemOqs3kxYBOltBTb +vFcLsLa4KYVu5Ge5YfB0Axfaem7PoP8IlMs8gxyojZ/r0o5hzxUcYlL/h8GeeoLW +PFFdiAS+UgxWINOqNXMCAwEAAaNTMFEwHQYDVR0OBBYEFLp9k4LmOnAR4ROrqhb+ +CFdbk2+oMB8GA1UdIwQYMBaAFLp9k4LmOnAR4ROrqhb+CFdbk2+oMA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGksycHsjGbXfWfuhQh+CvXk/A2v +MoNgiHtNMTGliVNgoVp1B1rj4x9xyZ8YrO8GAmv8jaCwCShd0B5Ul4aZVk1wglVv +lFAwb4IAZN9jv9+fw5BRzQ2tLhkVWIEwx6pZkhGhhjBvMaplLN5JwBtsdZorFbm7 +wuKiUKcFAM28acoOhCmOhgyNNBZpZn5wXaQDY43AthJOhitAV7vph4MPUkwIJnOh +MA5GJXEqS58TE9z9pkhQnn9598G8tmOXyA2erAoM9JAXM3EYHxVpoHBb9QRj6WAw +XVBo6qRXkwjNEM5CbnD4hVIBsdkOGsDrgd4Q5izQZ3x+jFNkdL/zPsXjJFw= +-----END CERTIFICATE----- + diff --git a/ansible_collections/community/windows/tests/integration/targets/win_certificate_info/files/subj-cert.pem b/ansible_collections/community/windows/tests/integration/targets/win_certificate_info/files/subj-cert.pem new file mode 100644 index 000000000..6d9ec39c7 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_certificate_info/files/subj-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIC0TCCAbkCCQC/MtOBa1UDpzANBgkqhkiG9w0BAQsFADAoMRkwFwYDVQQDDBBy +b290LmFuc2libGUuY29tMQswCQYDVQQGEwJVUzAgFw0xNzEyMTUwODU2MzBaGA8y +MDg2MDEwMjA4NTYzMFowKzEcMBoGA1UEAwwTc3ViamVjdC5hbnNpYmxlLmNvbTEL +MAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDszqdF +So3GlVP1xUnN4bSPrFRFiOl/Mqup0Zn5UJJUR9wLnRD+OLcq7kKin6hYqozSu7cC ++BnWQoq7vGSSNVqv7BqFMwzGJt9IBUQv0UqIQkA/duUdKdAiMn2PQRsNDnkWEbTj +4xsitItVNv84cDG0lkZBYyTgfyZlZLZWplkpUQkrZhoFCekZRJ+ODrqNW3W560rr +OUIh+HiQeBqocat6OdxgICBqpUh8EVo1iha3DXjGN08q5utg6gmbIl2VBaVJjfyd +wnUSqHylJwh6WCIEh+HXsn4ndfNWSN/fDqvi5I10V1j6Zos7yqQf8qAezUAm6eSq +hLgZz0odq9DsO4HHAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFK5mVIJ2D+kI0kk +sxnW4ibWFjzlYFYPYrZg+2JFIVTbKBg1YzyhuIKm0uztqRxQq5iLn/C/uponHoqF +7KDQI37KAJIQdgSva+mEuO9bZAXg/eegail2hN6np7HjOKlPu23s40dAbFrbcOWP +VbsBEPDP0HLv6OgbQWzNlE9HO1b7pX6ozk3q4ULO7IR85P6OHYsBBThL+qsOTzg/ +gVknuB9+n9hgNqZcAcXBLDetOM9aEmYJCGk0enYP5UGLYpseE+rTXFbRuHTPr1o6 +e8BetiSWS/wcrV4ZF5qr9NiYt5eD6JzTB5Rn5awxxj0FwMtrBu003lLQUWxsuTzz +35/RLY4= +-----END CERTIFICATE----- + diff --git a/ansible_collections/community/windows/tests/integration/targets/win_certificate_info/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_certificate_info/meta/main.yml new file mode 100644 index 000000000..9f37e96cd --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_certificate_info/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_remote_tmp_dir diff --git a/ansible_collections/community/windows/tests/integration/targets/win_certificate_info/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_certificate_info/tasks/main.yml new file mode 100644 index 000000000..a91b48108 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_certificate_info/tasks/main.yml @@ -0,0 +1,88 @@ +### keys in files/ have been generated with +# generate root private key +# openssl genrsa -aes256 -out enckey.pem 2048 +# openssl rsa -in envkey.pem -out root-key.pem +# +# generate root certificate +# openssl req -x509 -key root-key.pem -days 24855 -out root-vert.pem -subj "/CN=root.ansible.com/C=US" +# +# generate subject private key +# openssl genrsa -aes256 -out enckey.pem 2048 +# openssl rsa -in enckey.pem -out subj-key.pem +# +# generate subject certificate +# openssl req -new -key subj-key.pem -out cert.csr -subj "/CN=subject.ansible.com/C=US" +# openssl x509 -req -in cert.csr -CA root-cert.pem -CAkey root-key.pem -CAcreateserial -out subj-cert.pem -days 24855 +### +--- +- name: ensure test dir is present + ansible.windows.win_file: + path: '{{win_cert_dir}}\exported' + state: directory + +- name: copy across test cert files + ansible.windows.win_copy: + src: files/ + dest: '{{win_cert_dir}}' + +- name: subject cert imported to personal store + ansible.windows.win_certificate_store: + path: '{{win_cert_dir}}\subj-cert.pem' + state: present + store_name: My + +- name: root certificate imported to trusted root + ansible.windows.win_certificate_store: + path: '{{win_cert_dir}}\root-cert.pem' + store_name: Root + state: present + +- name: get raw root certificate + shell: 'cat root-cert.pem | grep "^[^-]"' + args: + chdir: '{{ role_path }}/files' + register: root_raw + delegate_to: localhost + +- name: get public key of root certificate + shell: 'openssl x509 -pubkey -noout -in root-cert.pem | grep "^[^-]"' + args: + chdir: '{{ role_path }}/files' + register: root_pub + delegate_to: localhost + +- name: get subject certificate + shell: 'cat subj-cert.pem | grep "^[^-]"' + args: + chdir: '{{ role_path }}/files' + register: subj_raw + delegate_to: localhost + +- name: get public key of subject certificate + shell: 'openssl x509 -pubkey -noout -in subj-cert.pem | grep "^[^-]"' + args: + chdir: '{{ role_path }}/files' + register: subj_pub + delegate_to: localhost + +- block: + - name: run tests + include_tasks: tests.yml + + always: + - name: ensure subject cert removed from personal store + ansible.windows.win_certificate_store: + thumbprint: '{{subj_thumbprint}}' + state: absent + store_name: My + + - name: ensure root cert removed from trusted root + ansible.windows.win_certificate_store: + thumbprint: '{{root_thumbprint}}' + state: absent + store_name: Root + + - name: ensure test dir is deleted + ansible.windows.win_file: + path: '{{win_cert_dir}}' + state: absent diff --git a/ansible_collections/community/windows/tests/integration/targets/win_certificate_info/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_certificate_info/tasks/tests.yml new file mode 100644 index 000000000..90eb0870b --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_certificate_info/tasks/tests.yml @@ -0,0 +1,90 @@ +--- + +- name: get stats on a store that doesn't exist + win_certificate_info: + store_name: teststore + register: test_store + +- name: ensure exists is false + assert: + that: + - test_store.exists == false + +- name: get stats on the root certificate store + win_certificate_info: + store_name: Root + register: root_store + +- name: at least one certificate is returned + assert: + that: + - "root_store.exists" + - "root_store.certificates | length > 0" + +- name: get stats on a certificate that doesn't exist + win_certificate_info: + thumbprint: ABC + register: actual + +- name: ensure exists is false + assert: + that: actual.exists == false + +- name: get stats on root certificate + win_certificate_info: + thumbprint: '{{ root_thumbprint }}' + store_name: Root + register: root_stats + +- name: root certificate stats returned are expected values + assert: + that: + - root_stats.exists + - root_stats.certificates[0].archived == false + - root_stats.certificates[0].dns_names == [ 'root.ansible.com' ] + - root_stats.certificates[0].extensions|count == 3 + - root_stats.certificates[0].has_private_key == false + - root_stats.certificates[0].issued_by == 'root.ansible.com' + - root_stats.certificates[0].issued_to == 'root.ansible.com' + - root_stats.certificates[0].issuer == 'C=US, CN=root.ansible.com' + - root_stats.certificates[0].path_length_constraint == 0 +# - root_stats.certificates[0].public_key == (root_pub.stdout_lines|join()) + - root_stats.certificates[0].raw == root_raw.stdout_lines|join() + - root_stats.certificates[0].serial_number == '00FD6F21D1A0309BFF' + - root_stats.certificates[0].signature_algorithm == 'sha256RSA' + - root_stats.certificates[0].ski == 'BA7D9382E63A7011E113ABAA16FE08575B936FA8' + - root_stats.certificates[0].subject == 'C=US, CN=root.ansible.com' + - root_stats.certificates[0].valid_from == 1513327172 + - root_stats.certificates[0].valid_from_iso8601 == '2017-12-15T08:39:32Z' + - root_stats.certificates[0].valid_to == 3660799172 + - root_stats.certificates[0].valid_to_iso8601 == '2086-01-02T08:39:32Z' + - root_stats.certificates[0].version == 3 + +- name: get stats on subject certificate + win_certificate_info: + thumbprint: '{{ subj_thumbprint }}' + register: subj_stats + +- name: subject certificate stats returned are expected values + assert: + that: + - subj_stats.exists + - subj_stats.certificates[0].archived == false + - subj_stats.certificates[0].dns_names == [ 'subject.ansible.com' ] + - subj_stats.certificates[0].extensions|count == 0 + - subj_stats.certificates[0].has_private_key == false + - subj_stats.certificates[0].issued_by == 'root.ansible.com' + - subj_stats.certificates[0].issued_to == 'subject.ansible.com' + - subj_stats.certificates[0].issuer == 'C=US, CN=root.ansible.com' + - subj_stats.certificates[0].path_length_constraint is undefined +# - subj_stats.certificates[0].public_key == subj_pub.stdout_lines|join() + - subj_stats.certificates[0].raw == subj_raw.stdout_lines|join() + - subj_stats.certificates[0].serial_number == '00BF32D3816B5503A7' + - subj_stats.certificates[0].signature_algorithm == 'sha256RSA' + - subj_stats.certificates[0].ski is undefined + - subj_stats.certificates[0].subject == 'C=US, CN=subject.ansible.com' + - subj_stats.certificates[0].valid_from == 1513328190 + - subj_stats.certificates[0].valid_from_iso8601 == '2017-12-15T08:56:30Z' + - subj_stats.certificates[0].valid_to == 3660800190 + - subj_stats.certificates[0].valid_to_iso8601 == '2086-01-02T08:56:30Z' + - subj_stats.certificates[0].version == 1 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_computer_description/aliases b/ansible_collections/community/windows/tests/integration/targets/win_computer_description/aliases new file mode 100644 index 000000000..423ce3910 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_computer_description/aliases @@ -0,0 +1 @@ +shippable/windows/group2 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_computer_description/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_computer_description/defaults/main.yml new file mode 100644 index 000000000..166a5248c --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_computer_description/defaults/main.yml @@ -0,0 +1,6 @@ +test_description: This is my computer +test_organization: iddqd +test_owner: BFG +test_description2: This is not my computer +test_organization2: idkfa +test_owner2: CACODEMON diff --git a/ansible_collections/community/windows/tests/integration/targets/win_computer_description/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_computer_description/tasks/main.yml new file mode 100644 index 000000000..4c3ce3294 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_computer_description/tasks/main.yml @@ -0,0 +1,200 @@ +--- +- name: Blank out description, organization and owner + win_computer_description: + description: "" + organization: "" + owner: "" + register: blank_set + check_mode: no + +- name: Change description, organization and owner in check mode + win_computer_description: + description: "{{ test_description }}" + organization: "{{ test_organization }}" + owner: "{{ test_owner }}" + register: change1_checkmode + check_mode: yes + +- name: Change description, organization and owner + win_computer_description: + description: "{{ test_description }}" + organization: "{{ test_organization }}" + owner: "{{ test_owner }}" + register: change1_set + check_mode: no + +- name: Change description, organization and owner 2nd time, there should be no change happening + win_computer_description: + description: "{{ test_description }}" + organization: "{{ test_organization }}" + owner: "{{ test_owner }}" + register: change1_set2 + check_mode: no + +- name: Assert that the above tasks returned the expected results + assert: + that: + - change1_checkmode is changed + - change1_set is changed + - change1_set2 is not changed + +- name: Get machine description + ansible.windows.win_shell: (Get-CimInstance -class "Win32_OperatingSystem").description + register: description1_changed + +- name: Get organization name + ansible.windows.win_reg_stat: + path: HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion + name: RegisteredOrganization + register: organization1_changed + +- name: Get owner + ansible.windows.win_reg_stat: + path: HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion + name: RegisteredOwner + register: owner1_changed + +- name: Assert that retrieved values are equal to the values provided in the variables + assert: + that: + - description1_changed.stdout == "{{ test_description }}\r\n" + - organization1_changed.value == "{{ test_organization }}" + - owner1_changed.value == "{{ test_owner }}" + +- name: Change description and owner only in check mode + win_computer_description: + description: "{{ test_description2 }}" + owner: "{{ test_owner2 }}" + register: change2_checkmode + check_mode: yes + +- name: Change description and owner only + win_computer_description: + description: "{{ test_description2 }}" + owner: "{{ test_owner2 }}" + register: change2_set + check_mode: no + +- name: Change description and owner only 2nd time, there should be no change happening + win_computer_description: + description: "{{ test_description2 }}" + owner: "{{ test_owner2 }}" + register: change2_set2 + check_mode: no + +- name: Assert that the above tasks returned the expected results + assert: + that: + - change2_checkmode is changed + - change2_set is changed + - change2_set2 is not changed + +- name: Get machine description + ansible.windows.win_shell: (Get-CimInstance -class "Win32_OperatingSystem").description + register: description2_changed + +- name: Get organization name + ansible.windows.win_reg_stat: + path: HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion + name: RegisteredOrganization + register: organization2_changed + +- name: Get owner + ansible.windows.win_reg_stat: + path: HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion + name: RegisteredOwner + register: owner2_changed + +- name: Assert that retrieved values are equal to the desired values + assert: + that: + - description2_changed.stdout == "{{ test_description2 }}\r\n" + - organization2_changed.value == "{{ test_organization }}" + - owner2_changed.value == "{{ test_owner2 }}" + +- name: Change organization only in check mode + win_computer_description: + organization: "{{ test_organization2 }}" + register: change3_checkmode + check_mode: yes + +- name: Change organization only in check mode + win_computer_description: + organization: "{{ test_organization2 }}" + register: change3_set + check_mode: no + +- name: Change organization only in check mode + win_computer_description: + organization: "{{ test_organization2 }}" + register: change3_set2 + check_mode: no + +- name: Assert that the above tasks returned the expected results + assert: + that: + - change3_checkmode is changed + - change3_set is changed + - change3_set2 is not changed + +- name: Get machine description + ansible.windows.win_shell: (Get-CimInstance -class "Win32_OperatingSystem").description + register: description3_changed + +- name: Get organization name + ansible.windows.win_reg_stat: + path: HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion + name: RegisteredOrganization + register: organization3_changed + +- name: Get owner + ansible.windows.win_reg_stat: + path: HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion + name: RegisteredOwner + register: owner3_changed + +- name: Assert that retrieved values are equal to the desired values + assert: + that: + - description3_changed.stdout == "{{ test_description2 }}\r\n" + - organization3_changed.value == "{{ test_organization2 }}" + - owner3_changed.value == "{{ test_owner2 }}" + +- name: Try to apply the same values again in check mode, there should be no change + win_computer_description: + description: "{{ test_description2 }}" + organization: "{{ test_organization2 }}" + owner: "{{ test_owner2 }}" + register: change4_checkmode + check_mode: yes + +- name: Try to apply the same values again, there should be no change + win_computer_description: + description: "{{ test_description2 }}" + organization: "{{ test_organization2 }}" + owner: "{{ test_owner2 }}" + register: change4_set + check_mode: no + +- name: Try to apply the same values again for 2nd time, there should be no change + win_computer_description: + description: "{{ test_description2 }}" + organization: "{{ test_organization2 }}" + owner: "{{ test_owner2 }}" + register: change4_set2 + check_mode: no + +- name: Assert that the above tasks returned the expected results + assert: + that: + - change4_checkmode is not changed + - change4_set is not changed + - change4_set2 is not changed + +- name: Blank the test values + win_computer_description: + description: '' + organization: '' + owner: '' + register: blank2_set + check_mode: no diff --git a/ansible_collections/community/windows/tests/integration/targets/win_credential/aliases b/ansible_collections/community/windows/tests/integration/targets/win_credential/aliases new file mode 100644 index 000000000..423ce3910 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_credential/aliases @@ -0,0 +1 @@ +shippable/windows/group2 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_credential/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_credential/defaults/main.yml new file mode 100644 index 000000000..c330762d4 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_credential/defaults/main.yml @@ -0,0 +1,19 @@ +# The certificate in files/cert.pfx was generated with the following commands +# +# cat > client.cnf <<EOL +# [ssl_client] +# basicConstraints = CA:FALSE +# nsCertType = client +# keyUsage = digitalSignature, keyEncipherment +# extendedKeyUsage = clientAuth +# EOL +# +# openssl genrsa -aes256 -passout pass:password1 -out cert.key 2048 +# openssl req -new -subj '/CN=ansible.domain.com' -key cert.key -out cert.req -passin pass:password1 +# openssl x509 -sha256 -req -in cert.req -days 24855 -signkey cert.key -out cert.crt -extensions ssl_client -extfile client.cnf -passin pass:password1 +# openssl pkcs12 -export -in cert.crt -inkey cert.key -out cert.pfx -passin pass:password1 -passout pass:password1 +--- +test_credential_dir: '{{ remote_tmp_dir }}\win_credential_manager' +test_hostname: ansible.domain.com +key_password: password1 +cert_thumbprint: 56841AAFDD19D7DF474BDA24D01D88BD8025A00A diff --git a/ansible_collections/community/windows/tests/integration/targets/win_credential/files/cert.pfx b/ansible_collections/community/windows/tests/integration/targets/win_credential/files/cert.pfx Binary files differnew file mode 100644 index 000000000..9cffb6696 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_credential/files/cert.pfx diff --git a/ansible_collections/community/windows/tests/integration/targets/win_credential/library/test_cred_facts.ps1 b/ansible_collections/community/windows/tests/integration/targets/win_credential/library/test_cred_facts.ps1 new file mode 100644 index 000000000..59206638f --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_credential/library/test_cred_facts.ps1 @@ -0,0 +1,501 @@ +#!powershell + +# Copyright: (c) 2018, 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.AddType + +$spec = @{ + options = @{ + name = @{ type = "str"; required = $true } + type = @{ type = "str"; required = $true; choices = @("domain_password", "domain_certificate", "generic_password", "generic_certificate") } + } + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$name = $module.Params.name +$type = $module.Params.type + +Add-CSharpType -AnsibleModule $module -References @' +using Microsoft.Win32.SafeHandles; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ansible.CredentialManager +{ + internal class NativeHelpers + { + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public class CREDENTIAL + { + public CredentialFlags Flags; + public CredentialType Type; + [MarshalAs(UnmanagedType.LPWStr)] public string TargetName; + [MarshalAs(UnmanagedType.LPWStr)] public string Comment; + public FILETIME LastWritten; + public UInt32 CredentialBlobSize; + public IntPtr CredentialBlob; + public CredentialPersist Persist; + public UInt32 AttributeCount; + public IntPtr Attributes; + [MarshalAs(UnmanagedType.LPWStr)] public string TargetAlias; + [MarshalAs(UnmanagedType.LPWStr)] public string UserName; + + public static explicit operator Credential(CREDENTIAL v) + { + byte[] secret = new byte[(int)v.CredentialBlobSize]; + if (v.CredentialBlob != IntPtr.Zero) + Marshal.Copy(v.CredentialBlob, secret, 0, secret.Length); + + List<CredentialAttribute> attributes = new List<CredentialAttribute>(); + if (v.AttributeCount > 0) + { + CREDENTIAL_ATTRIBUTE[] rawAttributes = new CREDENTIAL_ATTRIBUTE[v.AttributeCount]; + Credential.PtrToStructureArray(rawAttributes, v.Attributes); + attributes = rawAttributes.Select(x => (CredentialAttribute)x).ToList(); + } + + string userName = v.UserName; + if (v.Type == CredentialType.DomainCertificate || v.Type == CredentialType.GenericCertificate) + userName = Credential.UnmarshalCertificateCredential(userName); + + return new Credential + { + Type = v.Type, + TargetName = v.TargetName, + Comment = v.Comment, + LastWritten = (DateTimeOffset)v.LastWritten, + Secret = secret, + Persist = v.Persist, + Attributes = attributes, + TargetAlias = v.TargetAlias, + UserName = userName, + Loaded = true, + }; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct CREDENTIAL_ATTRIBUTE + { + [MarshalAs(UnmanagedType.LPWStr)] public string Keyword; + public UInt32 Flags; // Set to 0 and is reserved + public UInt32 ValueSize; + public IntPtr Value; + + public static explicit operator CredentialAttribute(CREDENTIAL_ATTRIBUTE v) + { + byte[] value = new byte[v.ValueSize]; + Marshal.Copy(v.Value, value, 0, (int)v.ValueSize); + + return new CredentialAttribute + { + Keyword = v.Keyword, + Flags = v.Flags, + Value = value, + }; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct FILETIME + { + internal UInt32 dwLowDateTime; + internal UInt32 dwHighDateTime; + + public static implicit operator long(FILETIME v) { return ((long)v.dwHighDateTime << 32) + v.dwLowDateTime; } + public static explicit operator DateTimeOffset(FILETIME v) { return DateTimeOffset.FromFileTime(v); } + public static explicit operator FILETIME(DateTimeOffset v) + { + return new FILETIME() + { + dwLowDateTime = (UInt32)v.ToFileTime(), + dwHighDateTime = ((UInt32)v.ToFileTime() >> 32), + }; + } + } + + [Flags] + public enum CredentialCreateFlags : uint + { + PreserveCredentialBlob = 1, + } + + [Flags] + public enum CredentialFlags + { + None = 0, + PromptNow = 2, + UsernameTarget = 4, + } + + public enum CredMarshalType : uint + { + CertCredential = 1, + UsernameTargetCredential, + BinaryBlobCredential, + UsernameForPackedCredential, + BinaryBlobForSystem, + } + } + + internal class NativeMethods + { + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool CredDeleteW( + [MarshalAs(UnmanagedType.LPWStr)] string TargetName, + CredentialType Type, + UInt32 Flags); + + [DllImport("advapi32.dll")] + public static extern void CredFree( + IntPtr Buffer); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool CredMarshalCredentialW( + NativeHelpers.CredMarshalType CredType, + SafeMemoryBuffer Credential, + out SafeCredentialBuffer MarshaledCredential); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool CredReadW( + [MarshalAs(UnmanagedType.LPWStr)] string TargetName, + CredentialType Type, + UInt32 Flags, + out SafeCredentialBuffer Credential); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool CredUnmarshalCredentialW( + [MarshalAs(UnmanagedType.LPWStr)] string MarshaledCredential, + out NativeHelpers.CredMarshalType CredType, + out SafeCredentialBuffer Credential); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool CredWriteW( + NativeHelpers.CREDENTIAL Credential, + NativeHelpers.CredentialCreateFlags Flags); + } + + internal class SafeCredentialBuffer : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeCredentialBuffer() : base(true) { } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + protected override bool ReleaseHandle() + { + NativeMethods.CredFree(handle); + return true; + } + } + + internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeMemoryBuffer() : base(true) { } + public SafeMemoryBuffer(int cb) : base(true) + { + 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; + } + } + + public class Win32Exception : System.ComponentModel.Win32Exception + { + private string _exception_msg; + public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { } + public Win32Exception(int errorCode, string message) : base(errorCode) + { + _exception_msg = String.Format("{0} - {1} (Win32 Error Code {2}: 0x{3})", message, base.Message, errorCode, errorCode.ToString("X8")); + } + public override string Message { get { return _exception_msg; } } + public static explicit operator Win32Exception(string message) { return new Win32Exception(message); } + } + + public enum CredentialPersist + { + Session = 1, + LocalMachine = 2, + Enterprise = 3, + } + + public enum CredentialType + { + Generic = 1, + DomainPassword = 2, + DomainCertificate = 3, + DomainVisiblePassword = 4, + GenericCertificate = 5, + DomainExtended = 6, + Maximum = 7, + MaximumEx = 1007, + } + + public class CredentialAttribute + { + public string Keyword; + public UInt32 Flags; + public byte[] Value; + } + + public class Credential + { + public CredentialType Type; + public string TargetName; + public string Comment; + public DateTimeOffset LastWritten; + public byte[] Secret; + public CredentialPersist Persist; + public List<CredentialAttribute> Attributes = new List<CredentialAttribute>(); + public string TargetAlias; + public string UserName; + + // Used to track whether the credential has been loaded into the store or not + public bool Loaded { get; internal set; } + + public void Delete() + { + if (!Loaded) + return; + + if (!NativeMethods.CredDeleteW(TargetName, Type, 0)) + throw new Win32Exception(String.Format("CredDeleteW({0}) failed", TargetName)); + Loaded = false; + } + + public void Write(bool preserveExisting) + { + string userName = UserName; + // Convert the certificate thumbprint to the string expected + if (Type == CredentialType.DomainCertificate || Type == CredentialType.GenericCertificate) + userName = Credential.MarshalCertificateCredential(userName); + + NativeHelpers.CREDENTIAL credential = new NativeHelpers.CREDENTIAL + { + Flags = NativeHelpers.CredentialFlags.None, + Type = Type, + TargetName = TargetName, + Comment = Comment, + LastWritten = new NativeHelpers.FILETIME(), + CredentialBlobSize = (UInt32)(Secret == null ? 0 : Secret.Length), + CredentialBlob = IntPtr.Zero, // Must be allocated and freed outside of this to ensure no memory leaks + Persist = Persist, + AttributeCount = (UInt32)(Attributes.Count), + Attributes = IntPtr.Zero, // Attributes must be allocated and freed outside of this to ensure no memory leaks + TargetAlias = TargetAlias, + UserName = userName, + }; + + using (SafeMemoryBuffer credentialBlob = new SafeMemoryBuffer((int)credential.CredentialBlobSize)) + { + if (Secret != null) + Marshal.Copy(Secret, 0, credentialBlob.DangerousGetHandle(), Secret.Length); + credential.CredentialBlob = credentialBlob.DangerousGetHandle(); + + // Store the CREDENTIAL_ATTRIBUTE value in a safe memory buffer and make sure we dispose in all cases + List<SafeMemoryBuffer> attributeBuffers = new List<SafeMemoryBuffer>(); + try + { + int attributeLength = Attributes.Sum(a => Marshal.SizeOf(typeof(NativeHelpers.CREDENTIAL_ATTRIBUTE))); + byte[] attributeBytes = new byte[attributeLength]; + int offset = 0; + foreach (CredentialAttribute attribute in Attributes) + { + SafeMemoryBuffer attributeBuffer = new SafeMemoryBuffer(attribute.Value.Length); + attributeBuffers.Add(attributeBuffer); + if (attribute.Value != null) + Marshal.Copy(attribute.Value, 0, attributeBuffer.DangerousGetHandle(), attribute.Value.Length); + + NativeHelpers.CREDENTIAL_ATTRIBUTE credentialAttribute = new NativeHelpers.CREDENTIAL_ATTRIBUTE + { + Keyword = attribute.Keyword, + Flags = attribute.Flags, + ValueSize = (UInt32)(attribute.Value == null ? 0 : attribute.Value.Length), + Value = attributeBuffer.DangerousGetHandle(), + }; + int attributeStructLength = Marshal.SizeOf(typeof(NativeHelpers.CREDENTIAL_ATTRIBUTE)); + + byte[] attrBytes = new byte[attributeStructLength]; + using (SafeMemoryBuffer tempBuffer = new SafeMemoryBuffer(attributeStructLength)) + { + Marshal.StructureToPtr(credentialAttribute, tempBuffer.DangerousGetHandle(), false); + Marshal.Copy(tempBuffer.DangerousGetHandle(), attrBytes, 0, attributeStructLength); + } + Buffer.BlockCopy(attrBytes, 0, attributeBytes, offset, attributeStructLength); + offset += attributeStructLength; + } + + using (SafeMemoryBuffer attributes = new SafeMemoryBuffer(attributeBytes.Length)) + { + if (attributeBytes.Length != 0) + Marshal.Copy(attributeBytes, 0, attributes.DangerousGetHandle(), attributeBytes.Length); + credential.Attributes = attributes.DangerousGetHandle(); + + NativeHelpers.CredentialCreateFlags createFlags = 0; + if (preserveExisting) + createFlags |= NativeHelpers.CredentialCreateFlags.PreserveCredentialBlob; + + if (!NativeMethods.CredWriteW(credential, createFlags)) + throw new Win32Exception(String.Format("CredWriteW({0}) failed", TargetName)); + } + } + finally + { + foreach (SafeMemoryBuffer attributeBuffer in attributeBuffers) + attributeBuffer.Dispose(); + } + } + Loaded = true; + } + + public static Credential GetCredential(string target, CredentialType type) + { + SafeCredentialBuffer buffer; + if (!NativeMethods.CredReadW(target, type, 0, out buffer)) + { + int lastErr = Marshal.GetLastWin32Error(); + + // Not running with CredSSP or Become so cannot manage the user's credentials + if (lastErr == 0x00000520) // ERROR_NO_SUCH_LOGON_SESSION + throw new InvalidOperationException("Failed to access the user's credential store, run the module with become or CredSSP"); + else if (lastErr == 0x00000490) // ERROR_NOT_FOUND + return null; + throw new Win32Exception(lastErr, "CredEnumerateW() failed"); + } + + using (buffer) + { + NativeHelpers.CREDENTIAL credential = (NativeHelpers.CREDENTIAL)Marshal.PtrToStructure( + buffer.DangerousGetHandle(), typeof(NativeHelpers.CREDENTIAL)); + return (Credential)credential; + } + } + + public static string MarshalCertificateCredential(string thumbprint) + { + // CredWriteW requires the UserName field to be the value of CredMarshalCredentialW() when writting a + // certificate auth. This converts the UserName property to the format required. + + // While CERT_CREDENTIAL_INFO is the correct structure, we manually marshal the data in order to + // support different cert hash lengths in the future. + // https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_cert_credential_info + int hexLength = thumbprint.Length; + byte[] credInfo = new byte[sizeof(UInt32) + (hexLength / 2)]; + + // First field is cbSize which is a UInt32 value denoting the size of the total structure + Array.Copy(BitConverter.GetBytes((UInt32)credInfo.Length), credInfo, sizeof(UInt32)); + + // Now copy the byte representation of the thumbprint to the rest of the struct bytes + for (int i = 0; i < hexLength; i += 2) + credInfo[sizeof(UInt32) + (i / 2)] = Convert.ToByte(thumbprint.Substring(i, 2), 16); + + IntPtr pCredInfo = Marshal.AllocHGlobal(credInfo.Length); + Marshal.Copy(credInfo, 0, pCredInfo, credInfo.Length); + SafeMemoryBuffer pCredential = new SafeMemoryBuffer(pCredInfo); + + NativeHelpers.CredMarshalType marshalType = NativeHelpers.CredMarshalType.CertCredential; + using (pCredential) + { + SafeCredentialBuffer marshaledCredential; + if (!NativeMethods.CredMarshalCredentialW(marshalType, pCredential, out marshaledCredential)) + throw new Win32Exception("CredMarshalCredentialW() failed"); + using (marshaledCredential) + return Marshal.PtrToStringUni(marshaledCredential.DangerousGetHandle()); + } + } + + public static string UnmarshalCertificateCredential(string value) + { + NativeHelpers.CredMarshalType credType; + SafeCredentialBuffer pCredInfo; + if (!NativeMethods.CredUnmarshalCredentialW(value, out credType, out pCredInfo)) + throw new Win32Exception("CredUnmarshalCredentialW() failed"); + + using (pCredInfo) + { + if (credType != NativeHelpers.CredMarshalType.CertCredential) + throw new InvalidOperationException(String.Format("Expected unmarshalled cred type of CertCredential, received {0}", credType)); + + byte[] structSizeBytes = new byte[sizeof(UInt32)]; + Marshal.Copy(pCredInfo.DangerousGetHandle(), structSizeBytes, 0, sizeof(UInt32)); + UInt32 structSize = BitConverter.ToUInt32(structSizeBytes, 0); + + byte[] certInfoBytes = new byte[structSize]; + Marshal.Copy(pCredInfo.DangerousGetHandle(), certInfoBytes, 0, certInfoBytes.Length); + + StringBuilder hex = new StringBuilder((certInfoBytes.Length - sizeof(UInt32)) * 2); + for (int i = 4; i < certInfoBytes.Length; i++) + hex.AppendFormat("{0:x2}", certInfoBytes[i]); + + return hex.ToString().ToUpperInvariant(); + } + } + + internal static void PtrToStructureArray<T>(T[] array, IntPtr ptr) + { + IntPtr ptrOffset = ptr; + for (int i = 0; i < array.Length; i++, ptrOffset = IntPtr.Add(ptrOffset, Marshal.SizeOf(typeof(T)))) + array[i] = (T)Marshal.PtrToStructure(ptrOffset, typeof(T)); + } + } +} +'@ + +$type = switch ($type) { + "domain_password" { [Ansible.CredentialManager.CredentialType]::DomainPassword } + "domain_certificate" { [Ansible.CredentialManager.CredentialType]::DomainCertificate } + "generic_password" { [Ansible.CredentialManager.CredentialType]::Generic } + "generic_certificate" { [Ansible.CredentialManager.CredentialType]::GenericCertificate } +} + +$credential = [Ansible.CredentialManager.Credential]::GetCredential($name, $type) +if ($null -ne $credential) { + $module.Result.exists = $true + $module.Result.alias = $credential.TargetAlias + $module.Result.attributes = [System.Collections.ArrayList]@() + $module.Result.comment = $credential.Comment + $module.Result.name = $credential.TargetName + $module.Result.persistence = $credential.Persist.ToString() + $module.Result.type = $credential.Type.ToString() + $module.Result.username = $credential.UserName + + if ($null -ne $credential.Secret) { + $module.Result.secret = [System.Convert]::ToBase64String($credential.Secret) + } + else { + $module.Result.secret = $null + } + + foreach ($attribute in $credential.Attributes) { + $attribute_info = @{ + name = $attribute.Keyword + } + if ($null -ne $attribute.Value) { + $attribute_info.data = [System.Convert]::ToBase64String($attribute.Value) + } + else { + $attribute_info.data = $null + } + $module.Result.attributes.Add($attribute_info) > $null + } +} +else { + $module.Result.exists = $false +} + +$module.ExitJson() + diff --git a/ansible_collections/community/windows/tests/integration/targets/win_credential/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_credential/meta/main.yml new file mode 100644 index 000000000..9f37e96cd --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_credential/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_remote_tmp_dir diff --git a/ansible_collections/community/windows/tests/integration/targets/win_credential/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_credential/tasks/main.yml new file mode 100644 index 000000000..1ec3bbb3a --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_credential/tasks/main.yml @@ -0,0 +1,64 @@ +--- +- name: ensure test dir is present + ansible.windows.win_file: + path: '{{ test_credential_dir }}' + state: directory + +- name: copy the pfx certificate + ansible.windows.win_copy: + src: cert.pfx + dest: '{{ test_credential_dir }}\cert.pfx' + +- name: import the pfx into the personal store + ansible.windows.win_certificate_store: + path: '{{ test_credential_dir }}\cert.pfx' + state: present + store_location: CurrentUser + store_name: My + password: '{{ key_password }}' + vars: &become_vars + ansible_become: True + ansible_become_method: runas + ansible_become_user: '{{ ansible_user }}' + ansible_become_pass: '{{ ansible_password }}' + +- name: ensure test credentials are removed before testing + win_credential: + name: '{{ test_hostname }}' + type: '{{ item }}' + state: absent + vars: *become_vars + with_items: + - domain_password + - domain_certificate + - generic_password + - generic_certificate + +- block: + - name: run tests + include_tasks: tests.yml + + always: + - name: remove the pfx from the personal store + ansible.windows.win_certificate_store: + state: absent + thumbprint: '{{ cert_thumbprint }}' + store_location: CurrentUser + store_name: My + + - name: remove test credentials + win_credential: + name: '{{ test_hostname }}' + type: '{{ item }}' + state: absent + vars: *become_vars + with_items: + - domain_password + - domain_certificate + - generic_password + - generic_certificate + + - name: remove test dir + ansible.windows.win_file: + path: '{{ test_credential_dir }}' + state: absent diff --git a/ansible_collections/community/windows/tests/integration/targets/win_credential/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_credential/tasks/tests.yml new file mode 100644 index 000000000..cec2cf023 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_credential/tasks/tests.yml @@ -0,0 +1,638 @@ +--- +- name: fail to run the module without become + win_credential: + name: '{{ test_hostname }}' + type: domain_password + username: DOMAIN\username + secret: password + state: present + register: fail_no_become + failed_when: '"Failed to access the user''s credential store, run the module with become" not in fail_no_become.msg' + +- name: create domain user credential (check mode) + win_credential: + name: '{{ test_hostname }}' + type: domain_password + username: DOMAIN\username + secret: password + state: present + register: domain_user_check + check_mode: True + vars: &become_vars + ansible_become: True + ansible_become_method: runas + ansible_become_user: '{{ ansible_user }}' + ansible_become_pass: '{{ ansible_password }}' + +- name: get result of create domain user credential (check mode) + test_cred_facts: + name: '{{ test_hostname }}' + type: domain_password + register: domain_user_actual_check + vars: *become_vars + +- name: asset create domain user credential (check mode) + assert: + that: + - domain_user_check is changed + - not domain_user_actual_check.exists + +- name: create domain user credential + win_credential: + name: '{{ test_hostname }}' + type: domain_password + username: DOMAIN\username + secret: password + state: present + register: domain_user + vars: *become_vars + +- name: get result of create domain user credential + test_cred_facts: + name: '{{ test_hostname }}' + type: domain_password + register: domain_user_actual + vars: *become_vars + +- name: asset create domain user credential + assert: + that: + - domain_user is changed + - domain_user_actual.exists + - domain_user_actual.alias == None + - domain_user_actual.attributes == [] + - domain_user_actual.comment == None + - domain_user_actual.name == test_hostname + - domain_user_actual.persistence == "LocalMachine" + - domain_user_actual.secret == "" + - domain_user_actual.type == "DomainPassword" + - domain_user_actual.username == "DOMAIN\\username" + +- name: create domain user credential again always update + win_credential: + name: '{{ test_hostname }}' + type: domain_password + username: DOMAIN\username + secret: password + state: present + register: domain_user_again_always + vars: *become_vars + +- name: create domain user credential again on_create + win_credential: + name: '{{ test_hostname }}' + type: domain_password + username: DOMAIN\username + secret: password + update_secret: on_create + state: present + register: domain_user_again_on_create + vars: *become_vars + +- name: assert create domain user credential again + assert: + that: + - domain_user_again_always is changed + - not domain_user_again_on_create is changed + +- name: update credential (check mode) + win_credential: + name: '{{ test_hostname }}' + type: domain_password + username: DOMAIN\username2 + alias: ansible + attributes: + - name: attribute 1 + data: attribute 1 value + - name: attribute 2 + data: '{{ "attribute 2 value" | b64encode }}' + data_format: base64 + comment: Credential comment + persistence: enterprise + state: present + register: update_cred_check + check_mode: True + vars: *become_vars + +- name: get result of update credential (check mode) + test_cred_facts: + name: '{{ test_hostname }}' + type: domain_password + register: update_cred_actual_check + vars: *become_vars + +- name: assert update credential (check mode) + assert: + that: + - update_cred_check is changed + - update_cred_actual_check.exists + - update_cred_actual_check.alias == None + - update_cred_actual_check.attributes == [] + - update_cred_actual_check.comment == None + - update_cred_actual_check.name == test_hostname + - update_cred_actual_check.persistence == "LocalMachine" + - update_cred_actual_check.secret == "" + - update_cred_actual_check.type == "DomainPassword" + - update_cred_actual_check.username == "DOMAIN\\username" + +- name: update credential + win_credential: + name: '{{ test_hostname }}' + type: domain_password + username: DOMAIN\username2 + alias: ansible + attributes: + - name: attribute 1 + data: attribute 1 value + - name: attribute 2 + data: '{{ "attribute 2 value" | b64encode }}' + data_format: base64 + comment: Credential comment + persistence: enterprise + state: present + register: update_cred + vars: *become_vars + +- name: get result of update credential + test_cred_facts: + name: '{{ test_hostname }}' + type: domain_password + register: update_cred_actual + vars: *become_vars + +- name: assert update credential + assert: + that: + - update_cred is changed + - update_cred_actual.exists + - update_cred_actual.alias == "ansible" + - update_cred_actual.attributes|count == 2 + - update_cred_actual.attributes[0].name == "attribute 1" + - update_cred_actual.attributes[0].data == "attribute 1 value"|b64encode + - update_cred_actual.attributes[1].name == "attribute 2" + - update_cred_actual.attributes[1].data == "attribute 2 value"|b64encode + - update_cred_actual.comment == "Credential comment" + - update_cred_actual.name == test_hostname + - update_cred_actual.persistence == "Enterprise" + - update_cred_actual.secret == "" + - update_cred_actual.type == "DomainPassword" + - update_cred_actual.username == "DOMAIN\\username2" + +- name: update credential again + win_credential: + name: '{{ test_hostname }}' + type: domain_password + username: DOMAIN\username2 + alias: ansible + attributes: + - name: attribute 1 + data: attribute 1 value + - name: attribute 2 + data: '{{ "attribute 2 value" | b64encode }}' + data_format: base64 + comment: Credential comment + persistence: enterprise + state: present + register: update_cred_again + vars: *become_vars + +- name: assert update credential again + assert: + that: + - not update_cred_again is changed + +- name: add new attribute + win_credential: + name: '{{ test_hostname }}' + type: domain_password + username: DOMAIN\username2 + alias: ansible + attributes: + - name: attribute 1 + data: attribute 1 value + - name: attribute 2 + data: '{{ "attribute 2 value" | b64encode }}' + data_format: base64 + - name: attribute 3 + data: attribute 3 value + comment: Credential comment + persistence: enterprise + state: present + register: add_attribute + vars: *become_vars + +- name: get result of add new attribute + test_cred_facts: + name: '{{ test_hostname }}' + type: domain_password + register: add_attribute_actual + vars: *become_vars + +- name: assert add new attribute + assert: + that: + - add_attribute is changed + - add_attribute_actual.attributes|count == 3 + - add_attribute_actual.attributes[0].name == "attribute 1" + - add_attribute_actual.attributes[0].data == "attribute 1 value"|b64encode + - add_attribute_actual.attributes[1].name == "attribute 2" + - add_attribute_actual.attributes[1].data == "attribute 2 value"|b64encode + - add_attribute_actual.attributes[2].name == "attribute 3" + - add_attribute_actual.attributes[2].data == "attribute 3 value"|b64encode + +- name: remove attribute + win_credential: + name: '{{ test_hostname }}' + type: domain_password + username: DOMAIN\username2 + alias: ansible + attributes: + - name: attribute 1 + data: attribute 1 value + - name: attribute 2 + data: '{{ "attribute 2 value" | b64encode }}' + data_format: base64 + comment: Credential comment + persistence: enterprise + state: present + register: remove_attribute + vars: *become_vars + +- name: get result of remove attribute + test_cred_facts: + name: '{{ test_hostname }}' + type: domain_password + register: remove_attribute_actual + vars: *become_vars + +- name: assert remove attribute + assert: + that: + - remove_attribute is changed + - remove_attribute_actual.attributes|count == 2 + - remove_attribute_actual.attributes[0].name == "attribute 1" + - remove_attribute_actual.attributes[0].data == "attribute 1 value"|b64encode + - remove_attribute_actual.attributes[1].name == "attribute 2" + - remove_attribute_actual.attributes[1].data == "attribute 2 value"|b64encode + +- name: edit attribute + win_credential: + name: '{{ test_hostname }}' + type: domain_password + username: DOMAIN\username2 + alias: ansible + attributes: + - name: attribute 1 + data: attribute 1 value new + - name: attribute 2 + data: '{{ "attribute 2 value" | b64encode }}' + data_format: base64 + comment: Credential comment + persistence: enterprise + state: present + register: edit_attribute + vars: *become_vars + +- name: get result of edit attribute + test_cred_facts: + name: '{{ test_hostname }}' + type: domain_password + register: edit_attribute_actual + vars: *become_vars + +- name: assert remove attribute + assert: + that: + - edit_attribute is changed + - edit_attribute_actual.attributes|count == 2 + - edit_attribute_actual.attributes[0].name == "attribute 1" + - edit_attribute_actual.attributes[0].data == "attribute 1 value new"|b64encode + - edit_attribute_actual.attributes[1].name == "attribute 2" + - edit_attribute_actual.attributes[1].data == "attribute 2 value"|b64encode + +- name: remove credential (check mode) + win_credential: + name: '{{ test_hostname }}' + type: domain_password + state: absent + register: remove_cred_check + check_mode: True + vars: *become_vars + +- name: get result of remove credential (check mode) + test_cred_facts: + name: '{{ test_hostname }}' + type: domain_password + register: remove_cred_actual_check + vars: *become_vars + +- name: assert remove credential (check mode) + assert: + that: + - remove_cred_check is changed + - remove_cred_actual_check.exists + +- name: remove credential + win_credential: + name: '{{ test_hostname }}' + type: domain_password + state: absent + register: remove_cred + vars: *become_vars + +- name: get result of remove credential + test_cred_facts: + name: '{{ test_hostname }}' + type: domain_password + register: remove_cred_actual + vars: *become_vars + +- name: assert remove credential + assert: + that: + - remove_cred is changed + - not remove_cred_actual.exists + +- name: remove credential again + win_credential: + name: '{{ test_hostname }}' + type: domain_password + state: absent + register: remove_cred_again + vars: *become_vars + +- name: assert remove credential again + assert: + that: + - not remove_cred_again is changed + +# https://github.com/ansible/ansible/issues/67278 +- name: create credential with wildcard + win_credential: + name: '*.{{ test_hostname }}' + type: domain_password + username: DOMAIN\username + secret: password + state: present + persistence: enterprise + register: wildcard_cred + vars: *become_vars + +- name: get result of create credential with wildcard + test_cred_facts: + name: '*.{{ test_hostname }}' + type: domain_password + register: wildcard_cred_actual + vars: *become_vars + +- name: assert create credential with wildcard + assert: + that: + - wildcard_cred is changed + - wildcard_cred_actual.name == '*.' ~ test_hostname + +- name: remove credential with wildcard + win_credential: + name: '*.{{ test_hostname }}' + type: domain_password + state: absent + register: wildcard_remove + vars: *become_vars + +- name: get result of remove credential with wildcard + test_cred_facts: + name: '*.{{ test_hostname }}' + type: domain_password + register: wildcard_remove_actual + vars: *become_vars + +- name: assert remove credential with wildcard + assert: + that: + - wildcard_remove is changed + - not wildcard_remove_actual.exists + +- name: create generic password (check mode) + win_credential: + name: '{{ test_hostname }}' + type: generic_password + persistence: enterprise + username: genericuser + secret: genericpass + state: present + register: generic_password_check + check_mode: True + vars: *become_vars + +- name: get result of create generic password (check mode) + test_cred_facts: + name: '{{ test_hostname }}' + type: generic_password + register: generic_password_actual_check + vars: *become_vars + +- name: assert result of create generic password (check mode) + assert: + that: + - generic_password_check is changed + - not generic_password_actual_check.exists + +- name: create generic password + win_credential: + name: '{{ test_hostname }}' + type: generic_password + persistence: enterprise + username: genericuser + secret: genericpass + state: present + register: generic_password + vars: *become_vars + +- name: get result of create generic password + test_cred_facts: + name: '{{ test_hostname }}' + type: generic_password + register: generic_password_actual + vars: *become_vars + +- name: set encoded password result + set_fact: + encoded_pass: '{{ "genericpass" | string | b64encode(encoding="utf-16-le") }}' + +- name: assert create generic password + assert: + that: + - generic_password is changed + - generic_password_actual.exists + - generic_password_actual.alias == None + - generic_password_actual.attributes == [] + - generic_password_actual.comment == None + - generic_password_actual.name == test_hostname + - generic_password_actual.persistence == "Enterprise" + - generic_password_actual.secret == encoded_pass + - generic_password_actual.type == "Generic" + - generic_password_actual.username == "genericuser" + +- name: create generic password again + win_credential: + name: '{{ test_hostname }}' + type: generic_password + persistence: enterprise + username: genericuser + secret: genericpass + state: present + register: generic_password_again + vars: *become_vars + +- name: assert create generic password again + assert: + that: + - not generic_password_again is changed + +- name: fail to create certificate cred with invalid thumbprint + win_credential: + name: '{{ test_hostname }}' + type: domain_certificate + username: 00112233445566778899AABBCCDDEEFF00112233 + state: present + register: fail_invalid_cert + failed_when: fail_invalid_cert.msg != "Failed to find certificate with the thumbprint 00112233445566778899AABBCCDDEEFF00112233 in the CurrentUser\\My store" + vars: *become_vars + +- name: create domain certificate cred (check mode) + win_credential: + name: '{{ test_hostname }}' + type: domain_certificate + username: '{{ cert_thumbprint }}' + state: present + register: domain_cert_check + check_mode: True + vars: *become_vars + +- name: get result of create domain certificate cred (check mode) + test_cred_facts: + name: '{{ test_hostname }}' + type: domain_certificate + register: domain_cert_actual_check + vars: *become_vars + +- name: assert create domain certificate cred (check mode) + assert: + that: + - domain_cert_check is changed + - not domain_cert_actual_check.exists + +- name: create domain certificate cred + win_credential: + name: '{{ test_hostname }}' + type: domain_certificate + username: '{{ cert_thumbprint }}' + state: present + register: domain_cert + vars: *become_vars + +- name: get result of create domain certificate cred + test_cred_facts: + name: '{{ test_hostname }}' + type: domain_certificate + register: domain_cert_actual + vars: *become_vars + +- name: assert create domain certificate cred + assert: + that: + - domain_cert is changed + - domain_cert_actual.exists + - domain_cert_actual.alias == None + - domain_cert_actual.attributes == [] + - domain_cert_actual.comment == None + - domain_cert_actual.name == test_hostname + - domain_cert_actual.persistence == "LocalMachine" + - domain_cert_actual.secret == "" + - domain_cert_actual.type == "DomainCertificate" + - domain_cert_actual.username == cert_thumbprint + +- name: create domain certificate cred again + win_credential: + name: '{{ test_hostname }}' + type: domain_certificate + username: '{{ cert_thumbprint }}' + state: present + register: domain_cert_again + vars: *become_vars + +- name: assert create domain certificate cred again + assert: + that: + - not domain_cert_again is changed + +- name: create generic certificate cred (check mode) + win_credential: + name: '{{ test_hostname }}' + type: generic_certificate + username: '{{ cert_thumbprint }}' + secret: '{{ "pin code" | b64encode }}' + secret_format: base64 + state: present + register: generic_cert_check + check_mode: True + vars: *become_vars + +- name: get result of create generic certificate cred (check mode) + test_cred_facts: + name: '{{ test_hostname }}' + type: generic_certificate + register: generic_cert_actual_check + vars: *become_vars + +- name: assert create generic certificate cred (check mode) + assert: + that: + - generic_cert_check is changed + - not generic_cert_actual_check.exists + +- name: create generic certificate cred + win_credential: + name: '{{ test_hostname }}' + type: generic_certificate + username: '{{ cert_thumbprint }}' + secret: '{{ "pin code" | b64encode }}' + secret_format: base64 + state: present + register: generic_cert + vars: *become_vars + +- name: get result of create generic certificate cred + test_cred_facts: + name: '{{ test_hostname }}' + type: generic_certificate + register: generic_cert_actual + vars: *become_vars + +- name: assert create generic certificate cred + assert: + that: + - generic_cert is changed + - generic_cert_actual.exists + - generic_cert_actual.alias == None + - generic_cert_actual.attributes == [] + - generic_cert_actual.comment == None + - generic_cert_actual.name == test_hostname + - generic_cert_actual.persistence == "LocalMachine" + - generic_cert_actual.secret == "pin code" | b64encode + - generic_cert_actual.type == "GenericCertificate" + - generic_cert_actual.username == cert_thumbprint + +- name: create generic certificate cred again + win_credential: + name: '{{ test_hostname }}' + type: generic_certificate + username: '{{ cert_thumbprint }}' + state: present + register: generic_cert_again + vars: *become_vars + +- name: assert create generic certificate cred again + assert: + that: + - not generic_cert_again is changed diff --git a/ansible_collections/community/windows/tests/integration/targets/win_data_deduplication/aliases b/ansible_collections/community/windows/tests/integration/targets/win_data_deduplication/aliases new file mode 100644 index 000000000..2a4f8cc66 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_data_deduplication/aliases @@ -0,0 +1,2 @@ +shippable/windows/group3 +skip/windows/2012 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_data_deduplication/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_data_deduplication/meta/main.yml new file mode 100644 index 000000000..9f37e96cd --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_data_deduplication/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_remote_tmp_dir diff --git a/ansible_collections/community/windows/tests/integration/targets/win_data_deduplication/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_data_deduplication/tasks/main.yml new file mode 100644 index 000000000..ae6be90ec --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_data_deduplication/tasks/main.yml @@ -0,0 +1,2 @@ +--- +- include: pre_test.yml diff --git a/ansible_collections/community/windows/tests/integration/targets/win_data_deduplication/tasks/pre_test.yml b/ansible_collections/community/windows/tests/integration/targets/win_data_deduplication/tasks/pre_test.yml new file mode 100644 index 000000000..d63468990 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_data_deduplication/tasks/pre_test.yml @@ -0,0 +1,40 @@ +--- +- set_fact: + AnsibleVhdx: '{{ remote_tmp_dir }}\AnsiblePart.vhdx' + +- name: Install FS-Data-Deduplication + ansible.windows.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 + ansible.windows.win_reboot: + reboot_timeout: 3600 + when: + - data_dedup_feat_reg.success + - data_dedup_feat_reg.reboot_required + +- name: Copy VHDX scripts + ansible.windows.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 + ansible.windows.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 + ansible.windows.win_command: diskpart.exe /s {{ remote_tmp_dir }}\partition_deletion_script.txt diff --git a/ansible_collections/community/windows/tests/integration/targets/win_data_deduplication/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_data_deduplication/tasks/tests.yml new file mode 100644 index 000000000..64a429271 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/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/ansible_collections/community/windows/tests/integration/targets/win_data_deduplication/templates/partition_creation_script.j2 b/ansible_collections/community/windows/tests/integration/targets/win_data_deduplication/templates/partition_creation_script.j2 new file mode 100644 index 000000000..8e47fda95 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/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/ansible_collections/community/windows/tests/integration/targets/win_data_deduplication/templates/partition_deletion_script.j2 b/ansible_collections/community/windows/tests/integration/targets/win_data_deduplication/templates/partition_deletion_script.j2 new file mode 100644 index 000000000..c2be9cd14 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_data_deduplication/templates/partition_deletion_script.j2 @@ -0,0 +1,3 @@ +select vdisk file="{{ AnsibleVhdx }}" + +detach vdisk diff --git a/ansible_collections/community/windows/tests/integration/targets/win_dhcp_lease/aliases b/ansible_collections/community/windows/tests/integration/targets/win_dhcp_lease/aliases new file mode 100644 index 000000000..215e0b069 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_dhcp_lease/aliases @@ -0,0 +1 @@ +shippable/windows/group4 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_dhcp_lease/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_dhcp_lease/defaults/main.yml new file mode 100644 index 000000000..4eb43278e --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_dhcp_lease/defaults/main.yml @@ -0,0 +1,8 @@ +--- +dhcp_lease_ip: 172.16.98.230 +dhcp_scope_id: 172.16.98.0 +dhcp_scope_start: 172.16.98.1 +dhcp_scope_end: 172.16.98.254 +dhcp_scope_subnet_mask: 255.255.255.0 +dhcp_lease_mac: 0A-0B-0C-04-05-AA +dhcp_lease_hostname: fancy-reservation diff --git a/ansible_collections/community/windows/tests/integration/targets/win_dhcp_lease/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_dhcp_lease/tasks/main.yml new file mode 100644 index 000000000..d9b36a287 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_dhcp_lease/tasks/main.yml @@ -0,0 +1,27 @@ +--- +- block: + - name: Check DHCP Service/Role Install State + ansible.windows.win_feature: + name: DHCP + state: present + include_management_tools: yes + register: dhcp_role + + - name: Reboot if Necessary + ansible.windows.win_reboot: + when: dhcp_role.reboot_required + + - name: Add the DHCP scope + ansible.windows.win_shell: | + Add-DhcpServerv4Scope -Name "TestNetwork" -StartRange {{ dhcp_scope_start }} -EndRange {{dhcp_scope_end }} -SubnetMask {{ dhcp_scope_subnet_mask }} + + - name: Run tests without check mode + include_tasks: tests.yml + + - name: Run tests in check mode + include_tasks: tests_checkmode.yml + + always: + - name: Remove the DHCP scope + ansible.windows.win_shell: | + Remove-DhcpServerv4Scope -ScopeId {{ dhcp_scope_id }} -Force
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_dhcp_lease/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_dhcp_lease/tasks/tests.yml new file mode 100644 index 000000000..98bcc50ab --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_dhcp_lease/tasks/tests.yml @@ -0,0 +1,108 @@ +--- +- name: Remove DHCP Address by IP + win_dhcp_lease: + state: absent + ip: "{{ dhcp_lease_ip }}" + +- name: Remove DHCP Address by IP (Idempotentcy Check) - Changed should equal false + win_dhcp_lease: + state: absent + ip: "{{ dhcp_lease_ip }}" + register: remove_reservation_ip + failed_when: remove_reservation_ip.changed != false + +- name: Create New DHCP Lease + win_dhcp_lease: + type: lease + ip: "{{ dhcp_lease_ip }}" + scope_id: "{{ dhcp_scope_id }}" + mac: "{{ dhcp_lease_mac }}" + dns_hostname: "{{ dhcp_lease_hostname }}" + dns_regtype: noreg + description: This is a description! + +- name: Create New DHCP Lease (Idempotentcy Check) - Changed should equal false + win_dhcp_lease: + type: lease + ip: "{{ dhcp_lease_ip }}" + scope_id: "{{ dhcp_scope_id }}" + mac: "{{ dhcp_lease_mac }}" + dns_hostname: "{{ dhcp_lease_hostname }}" + dns_regtype: noreg + description: This is a description! + register: create_lease + failed_when: create_lease.changed != false + +- name: Validate the Lease + ansible.windows.win_shell: | + Get-DhcpServerv4Scope | Get-DhcpServerv4Lease | Where-Object IPAddress -eq {{ dhcp_lease_ip }} + register: validate_lease_out + failed_when: validate_lease_out.stdout == "" + +- name: Convert Lease to Reservation + win_dhcp_lease: + type: reservation + ip: "{{ dhcp_lease_ip }}" + +- name: Convert Lease to Reservation (Idempotentcy Check) - Changed should equal false + win_dhcp_lease: + type: reservation + ip: "{{ dhcp_lease_ip }}" + register: convert_lease_to_reservation + failed_when: convert_lease_to_reservation.changed != false + +- name: Validate the Reservation + ansible.windows.win_shell: | + Get-DhcpServerv4Scope | Get-DhcpServerv4Reservation | Where-Object IPAddress -eq {{ dhcp_lease_ip }} + register: validate_reservation_out + failed_when: validate_reservation_out.stdout == "" + +- name: Update Reservation Description + win_dhcp_lease: + type: reservation + mac: "{{ dhcp_lease_mac }}" + description: Changed Description! + +- name: Update Reservation Description (Idempotentcy Check) - Changed should equal false + win_dhcp_lease: + type: reservation + mac: "{{ dhcp_lease_mac }}" + description: Changed Description! + register: update_reservation_description + failed_when: update_reservation_description.changed != false + +- name: Validate the Description + ansible.windows.win_shell: | + Get-DhcpServerv4Scope | Get-DhcpServerv4Lease | Where-Object {($_.ClientId -eq "{{ dhcp_lease_mac }}") -and ($_.Description -eq "Changed Description!")} + register: validate_description_out + failed_when: validate_description_out.stdout == "" + +- name: Convert Reservation to Lease + win_dhcp_lease: + type: lease + ip: "{{ dhcp_lease_ip }}" + +- name: Convert Reservation to Lease (Idempotentcy Check) - Changed should equal false + win_dhcp_lease: + type: lease + ip: "{{ dhcp_lease_ip }}" + register: convert_reservation_to_lease + failed_when: convert_reservation_to_lease.changed != false + +- name: Remove DHCP Reservation + win_dhcp_lease: + state: absent + mac: "{{ dhcp_lease_mac }}" + +- name: Remove DHCP Reservation (Idempotentcy Check) - Changed should equal false + win_dhcp_lease: + state: absent + mac: "{{ dhcp_lease_mac }}" + register: remove_reservation + failed_when: remove_reservation.changed != false + +- name: Validate the State + ansible.windows.win_shell: | + Get-DhcpServerv4Scope | Get-DhcpServerv4Reservation | Where-Object IPAddress -eq {{ dhcp_lease_ip }} + register: validate_state_out + failed_when: validate_state_out.stdout != ""
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_dhcp_lease/tasks/tests_checkmode.yml b/ansible_collections/community/windows/tests/integration/targets/win_dhcp_lease/tasks/tests_checkmode.yml new file mode 100644 index 000000000..61f6ab5df --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_dhcp_lease/tasks/tests_checkmode.yml @@ -0,0 +1,52 @@ +--- +- name: Check DHCP Service/Role Install State + ansible.windows.win_feature: + name: DHCP + state: present + include_management_tools: yes + register: dhcp_role + check_mode: true + +- name: Reboot if Necessary + ansible.windows.win_reboot: + when: dhcp_role.reboot_required + check_mode: true + +- name: Remove DHCP Address by IP + win_dhcp_lease: + state: absent + ip: "{{ dhcp_lease_ip }}" + check_mode: true + +- name: Create Lease + win_dhcp_lease: + type: lease + ip: "{{ dhcp_lease_ip }}" + scope_id: "{{ dhcp_scope_id }}" + mac: "{{ dhcp_lease_mac }}" + dns_hostname: "{{ dhcp_lease_hostname }}" + dns_regtype: noreg + description: This is a description! + check_mode: true + +- name: Create Reservation + win_dhcp_lease: + type: reservation + ip: "{{ dhcp_lease_ip }}" + mac: "{{ dhcp_lease_mac }}" + scope_id: "{{ dhcp_scope_id }}" + check_mode: true + +- name: Create Reservation w/Description + win_dhcp_lease: + type: reservation + ip: "{{ dhcp_lease_ip }}" + mac: "{{ dhcp_lease_mac }}" + scope_id: "{{ dhcp_scope_id }}" + description: This is a Description! + check_mode: true + +- name: Remove DHCP Reservation by MAC + win_dhcp_lease: + state: absent + mac: "{{ dhcp_lease_mac }}" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_disk_facts/aliases b/ansible_collections/community/windows/tests/integration/targets/win_disk_facts/aliases new file mode 100644 index 000000000..e4adbabbe --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_disk_facts/aliases @@ -0,0 +1,3 @@ +shippable/windows/group2 +skip/windows/2008 # The Storage PowerShell module was introduced in W2K12 +skip/windows/2008-R2 # The Storage PowerShell module was introduced in W2K12 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_disk_facts/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_disk_facts/tasks/main.yml new file mode 100644 index 000000000..f1873efa7 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_disk_facts/tasks/main.yml @@ -0,0 +1,13 @@ +# NOTE: The win_disk_facts module only works on Win2012R2+ + +- name: check whether storage module is available (windows 2008 r2 or later) + ansible.windows.win_shell: '(Get-Module -Name Storage -ListAvailable | Measure-Object).Count -eq 1' + register: win_feature_has_storage_module + changed_when: false + +- name: Only run tests when Windows is capable + when: win_feature_has_storage_module.stdout | trim | bool == True + block: + + - name: Test in normal mode + include: tests.yml diff --git a/ansible_collections/community/windows/tests/integration/targets/win_disk_facts/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_disk_facts/tasks/tests.yml new file mode 100644 index 000000000..36d9a39e6 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_disk_facts/tasks/tests.yml @@ -0,0 +1,89 @@ +- name: get disk facts on the target + win_disk_facts: + register: disks_found + +- name: assert disk facts + assert: + that: + - disks_found.changed == false + - disks_found.ansible_facts.ansible_disks[0].number is defined + - disks_found.ansible_facts.ansible_disks[0].guid is defined + - disks_found.ansible_facts.ansible_disks[0].location is defined + - disks_found.ansible_facts.ansible_disks[0].path is defined + - disks_found.ansible_facts.ansible_disks[0].read_only is defined + - disks_found.ansible_facts.ansible_disks[0].clustered is defined + - disks_found.ansible_facts.ansible_disks[0].bootable is defined + - disks_found.ansible_facts.ansible_disks[0].physical_disk.size is defined + - disks_found.ansible_facts.ansible_disks[0].physical_disk.operational_status is defined + - disks_found.ansible_facts.ansible_disks[0].win32_disk_drive is defined + - disks_found.ansible_facts.ansible_disks[0].partitions is defined + - disks_found.ansible_facts.ansible_disks[0].partitions[0].volumes is defined + +- name: get disk partition facts on the target + win_disk_facts: + filter: + - partitions + register: disks_partitions_found + +- name: assert partitions disk facts + assert: + that: + - disks_partitions_found.changed == false + - disks_partitions_found.ansible_facts.ansible_disks[0].partitions is defined + +- name: get disk volume and partition facts on the target + win_disk_facts: + filter: + - volumes + register: disks_volumes_found + +- name: assert volume and partition disk facts + assert: + that: + - disks_volumes_found.changed == false + - disks_volumes_found.ansible_facts.ansible_disks[0].partitions is defined + - disks_volumes_found.ansible_facts.ansible_disks[0].partitions[0].volumes is defined + + +- name: get disk physical_disk facts on the target + win_disk_facts: + filter: + - physical_disk + register: physical_disk_found + +- name: assert physical_disk disk facts + assert: + that: + - physical_disk_found.changed == false + - physical_disk_found.ansible_facts.ansible_disks[0].physical_disk is defined + +- name: get disk virtual_disk facts on the target + win_disk_facts: + filter: + - virtual_disk + register: virtual_disk_found + +- name: check if virtual_disk should be found + ansible.windows.win_shell: | + if (Get-VirtualDisk){$true}else{$false} + register: virtual_available + changed_when: false + +- name: assert virtual_disk disk facts + assert: + that: + - virtual_disk_found.changed == false + - virtual_disk_found.ansible_facts.ansible_disks[0].virtual_disk is defined + when: virtual_available.stdout == "True" + +- name: get disk win32_disk_drive facts on the target + win_disk_facts: + filter: + - win32_disk_drive + register: win32_disk_drive_found + +- name: assert win32_disk_drive disk facts + assert: + that: + - win32_disk_drive_found.changed == false + - win32_disk_drive_found.ansible_facts.ansible_disks[0].win32_disk_drive is defined diff --git a/ansible_collections/community/windows/tests/integration/targets/win_dns_record/aliases b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/aliases new file mode 100644 index 000000000..215e0b069 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/aliases @@ -0,0 +1 @@ +shippable/windows/group4 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_dns_record/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/defaults/main.yml new file mode 100644 index 000000000..496102481 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/defaults/main.yml @@ -0,0 +1,3 @@ +win_dns_record_zone: test.ansible.local +win_dns_record_revzone: 0.0.255.in-addr.arpa +win_dns_record_revzone_network: 255.0.0.0/24 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/clean.yml b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/clean.yml new file mode 100644 index 000000000..29dc1cd97 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/clean.yml @@ -0,0 +1,17 @@ +- name: Remove test zone, if present + ansible.windows.win_shell: | + $zone = '{{ item }}' + $fail_on_missing = '{{ fail_on_missing | default(true) }}' + + Trap { If (-not $fail_on_missing) { continue } } + Remove-DnsServerZone -Name $zone -Force + + # win_file could also do this, but it would need to know where the + # SystemRoot is located via fact gathering, which we cannot assume. + Trap { If (-not $fail_on_missing) { continue } } + Remove-Item -Path $env:SystemRoot\system32\dns\$zone.dns + + $true # so pipeline exits cleanly if an error was ignored above + loop: + - '{{ win_dns_record_zone }}' + - '{{ win_dns_record_revzone }}' diff --git a/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/main.yml new file mode 100644 index 000000000..9b06a65b0 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/main.yml @@ -0,0 +1,12 @@ +# We do an explicit OS version check here *INSTEAD OF* the usual test for +# cmdlet existence. That's because a cmdlet test here won't work without first +# installing the DNS feature, but we don't want to install the feature on OS' +# that can't be supported anyway. Hence this fallback to an explicit OS version +# test. +- name: check OS version is supported + ansible.windows.win_shell: 'if ([Environment]::OSVersion.Version -ge [Version]"6.2") { $true } else { $false }' + register: os_supported + +- name: run tests on supported hosts + include: tests.yml + when: os_supported.stdout | trim | bool diff --git a/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests-A.yml b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests-A.yml new file mode 100644 index 000000000..dad1d82ea --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests-A.yml @@ -0,0 +1,294 @@ +- name: 'TYPE=A - creation (check mode)' + win_dns_record: + { zone: '{{ win_dns_record_zone }}', name: test1, value: 1.2.3.4, type: A } + register: cmd_result + check_mode: yes + +- name: 'TYPE=A - creation get results (check mode)' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType A -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=A - creation check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'absent\r\n' + +- name: 'TYPE=A - creation' + win_dns_record: + { zone: '{{ win_dns_record_zone }}', name: test1, value: 1.2.3.4, type: A } + register: cmd_result + +- name: 'TYPE=A - creation get results' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType A -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty IPv4Address | Select -ExpandProperty IPAddressToString" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=A - creation check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '1.2.3.4\r\n' + +- name: 'TYPE=A - creation (idempotent)' + win_dns_record: + { zone: '{{ win_dns_record_zone }}', name: test1, value: 1.2.3.4, type: A } + register: cmd_result + +- name: 'TYPE=A - creation get results (idempotent)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType A -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty IPv4Address | Select -ExpandProperty IPAddressToString" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=A - creation check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == '1.2.3.4\r\n' + +- name: 'TYPE=A - update address (check mode)' + win_dns_record: + { zone: '{{ win_dns_record_zone }}', name: test1, value: 5.6.7.8, type: A } + register: cmd_result + check_mode: yes + +- name: 'TYPE=A - update address get results (check mode)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType A -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty IPv4Address | Select -ExpandProperty IPAddressToString" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=A - update address check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '1.2.3.4\r\n' + +- name: 'TYPE=A - update address' + win_dns_record: + { zone: '{{ win_dns_record_zone }}', name: test1, value: 5.6.7.8, type: A } + register: cmd_result + +- name: 'TYPE=A - update address get results' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType A -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty IPv4Address | Select -ExpandProperty IPAddressToString" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=A - update address check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '5.6.7.8\r\n' + +- name: 'TYPE=A - update address (idempotent)' + win_dns_record: + { zone: '{{ win_dns_record_zone }}', name: test1, value: 5.6.7.8, type: A } + register: cmd_result + +- name: 'TYPE=A - update address get results (idempotent)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType A -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty IPv4Address | Select -ExpandProperty IPAddressToString" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=A - update address check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == '5.6.7.8\r\n' + +- name: 'TYPE=A - update TTL (check mode)' + win_dns_record: + { + zone: '{{ win_dns_record_zone }}', + name: test1, + value: 5.6.7.8, + ttl: 7200, + type: A, + } + register: cmd_result + check_mode: yes + +- name: 'TYPE=A - update TTL get results (check mode)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType A -Node -ErrorAction:Ignore | Select -ExpandProperty TimeToLive | Select -ExpandProperty TotalSeconds" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=A - update TTL check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '3600\r\n' + +- name: 'TYPE=A - update TTL' + win_dns_record: + { + zone: '{{ win_dns_record_zone }}', + name: test1, + value: 5.6.7.8, + ttl: 7200, + type: A, + } + register: cmd_result + +- name: 'TYPE=A - update TTL get results' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType A -Node -ErrorAction:Ignore | Select -ExpandProperty TimeToLive | Select -ExpandProperty TotalSeconds" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=A - update TTL check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '7200\r\n' + +- name: 'TYPE=A - update TTL (idempotent)' + win_dns_record: + { + zone: '{{ win_dns_record_zone }}', + name: test1, + value: 5.6.7.8, + ttl: 7200, + type: A, + } + register: cmd_result + +- name: 'TYPE=A - update TTL get results (idempotent)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType A -Node -ErrorAction:Ignore | Select -ExpandProperty TimeToLive | Select -ExpandProperty TotalSeconds" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=A - update TTL check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == '7200\r\n' + +- name: 'TYPE=A - remove record (check mode)' + win_dns_record: + { zone: '{{ win_dns_record_zone }}', name: test1, type: A, state: absent } + register: cmd_result + check_mode: yes + +- name: 'TYPE=A - remove record get results (check mode)' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType A -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=A - remove record check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'exists\r\n' + +- name: 'TYPE=A - remove record' + win_dns_record: + { zone: '{{ win_dns_record_zone }}', name: test1, type: A, state: absent } + register: cmd_result + +- name: 'TYPE=A - remove record get results' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType A -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=A - remove record check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'absent\r\n' + +- name: 'TYPE=A - remove record (idempotent)' + win_dns_record: + { zone: '{{ win_dns_record_zone }}', name: test1, type: A, state: absent } + register: cmd_result + +- name: 'TYPE=A - remove record get results (idempotent)' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType A -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=A - remove record check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == 'absent\r\n' + +# tests for aging +# ----------------------------------------------------------------------------- +# create aging record +- name: 'TYPE=A - add aging record' + win_dns_record: + zone: '{{ win_dns_record_zone }}' + name: testaging + value: 1.2.3.4 + aging: true + type: A + state: present + register: cmd_result + +- name: 'Type=A - check if record is actually aging' + ansible.windows.win_command: powershell.exe "If ($null -ne (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'testaging' -RRType A).Timestamp) {'aging'} else {'not aging'}" + register: cmd_result_actual + +- name: 'TYPE=A - check aging record results' + ansible.builtin.assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'aging\r\n' + +# check idempotency +- name: 'TYPE=A - check aging record (idempotency)' + win_dns_record: + zone: '{{ win_dns_record_zone }}' + name: testaging + value: 1.2.3.4 + aging: true + type: A + state: present + register: cmd_result + +- name: 'TYPE=A - check aging record' + ansible.builtin.assert: + that: + - cmd_result is not changed + +# change aging attribute +- name: 'TYPE=A - aging record (change aging)' + win_dns_record: + zone: '{{ win_dns_record_zone }}' + name: testaging + aging: false + value: 1.2.3.4 + type: A + state: present + register: cmd_result + +- name: Type=A - check if record is not aging + ansible.windows.win_command: powershell.exe "If ($null -ne (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'testaging' -RRType A).Timestamp) {'aging'} else {'not aging'}" + register: cmd_result_actual + +- name: 'TYPE=A - check not aging record results' + ansible.builtin.assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'not aging\r\n' + +# remove record again +- name: 'TYPE=A - remove record' + win_dns_record: + zone: '{{ win_dns_record_zone }}' + name: testaging + type: A + state: absent + register: cmd_result + +- name: 'TYPE=A - remove record get results' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'testaging' -RRType A -Node -ErrorAction:Ignore) {'exists'} else {'absent'}" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=A - remove record check results' + ansible.builtin.assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'absent\r\n' +# ----------------------------------------------------------------------------- diff --git a/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests-AAAA.yml b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests-AAAA.yml new file mode 100644 index 000000000..9db8ffb64 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests-AAAA.yml @@ -0,0 +1,186 @@ +- name: 'TYPE=AAAA - creation (check mode)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: '2001:db8::1', type: AAAA} + register: cmd_result + check_mode: yes + +- name: 'TYPE=AAAA - creation get results (check mode)' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType AAAA -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=AAAA - creation check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'absent\r\n' + +- name: 'TYPE=AAAA - creation' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: '2001:db8::1', type: AAAA} + register: cmd_result + +- name: 'TYPE=AAAA - creation get results' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType AAAA -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty IPv6Address | Select -ExpandProperty IPAddressToString" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=AAAA - creation check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '2001:db8::1\r\n' + +- name: 'TYPE=AAAA - creation (idempotent)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: '2001:db8::1', type: AAAA} + register: cmd_result + +- name: 'TYPE=AAAA - creation get results (idempotent)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType AAAA -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty IPv6Address | Select -ExpandProperty IPAddressToString" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=AAAA - creation check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == '2001:db8::1\r\n' + + +- name: 'TYPE=AAAA - update address (check mode)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: '2001:db8::2', type: AAAA} + register: cmd_result + check_mode: yes + +- name: 'TYPE=AAAA - update address get results (check mode)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType AAAA -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty IPv6Address | Select -ExpandProperty IPAddressToString" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=AAAA - update address check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '2001:db8::1\r\n' + +- name: 'TYPE=AAAA - update address' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: '2001:db8::2', type: AAAA} + register: cmd_result + +- name: 'TYPE=AAAA - update address get results' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType AAAA -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty IPv6Address | Select -ExpandProperty IPAddressToString" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=AAAA - update address check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '2001:db8::2\r\n' + +- name: 'TYPE=AAAA - update address (idempotent)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: '2001:db8::2', type: AAAA} + register: cmd_result + +- name: 'TYPE=AAAA - update address get results (idempotent)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType AAAA -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty IPv6Address | Select -ExpandProperty IPAddressToString" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=AAAA - update address check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == '2001:db8::2\r\n' + + +- name: 'TYPE=AAAA - update TTL (check mode)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: '2001:db8::2', ttl: 7200, type: AAAA} + register: cmd_result + check_mode: yes + +- name: 'TYPE=AAAA - update TTL get results (check mode)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType AAAA -Node -ErrorAction:Ignore | Select -ExpandProperty TimeToLive | Select -ExpandProperty TotalSeconds" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=AAAA - update TTL check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '3600\r\n' + +- name: 'TYPE=AAAA - update TTL' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: '2001:db8::2', ttl: 7200, type: AAAA} + register: cmd_result + +- name: 'TYPE=AAAA - update TTL get results' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType AAAA -Node -ErrorAction:Ignore | Select -ExpandProperty TimeToLive | Select -ExpandProperty TotalSeconds" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=AAAA - update TTL check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '7200\r\n' + +- name: 'TYPE=AAAA - update address (idempotent)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: '2001:db8::2', ttl: 7200, type: AAAA} + register: cmd_result + +- name: 'TYPE=AAAA - update address get results (idempotent)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType AAAA -Node -ErrorAction:Ignore | Select -ExpandProperty TimeToLive | Select -ExpandProperty TotalSeconds" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=AAAA - update address check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == '7200\r\n' + + +- name: 'TYPE=AAAA - remove record (check mode)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, type: AAAA, state: absent} + register: cmd_result + check_mode: yes + +- name: 'TYPE=AAAA - remove record get results (check mode)' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType AAAA -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=AAAA - remove record check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'exists\r\n' + +- name: 'TYPE=AAAA - remove record' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, type: AAAA, state: absent} + register: cmd_result + +- name: 'TYPE=AAAA - remove record get results' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType AAAA -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=AAAA - remove record check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'absent\r\n' + +- name: 'TYPE=AAAA - remove record (idempotent)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, type: AAAA, state: absent} + register: cmd_result + +- name: 'TYPE=AAAA - remove record get results (idempotent)' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType AAAA -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=AAAA - remove record check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == 'absent\r\n' diff --git a/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests-CNAME.yml b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests-CNAME.yml new file mode 100644 index 000000000..d806b0a4e --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests-CNAME.yml @@ -0,0 +1,205 @@ +- name: 'TYPE=CNAME - creation (check mode)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: www.ansible.com, type: CNAME} + register: cmd_result + check_mode: yes + +- name: 'TYPE=CNAME - creation get results (check mode)' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType CNAME -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=CNAME - creation check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'absent\r\n' + +- name: 'TYPE=CNAME - creation' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: www.ansible.com, type: CNAME} + register: cmd_result + +- name: 'TYPE=CNAME - creation get results' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType CNAME -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty HostNameAlias" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=CNAME - creation check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'www.ansible.com.\r\n' + +- name: 'TYPE=CNAME - creation (idempotent)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: www.ansible.com, type: CNAME} + register: cmd_result + +- name: 'TYPE=CNAME - creation get results (idempotent)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType CNAME -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty HostNameAlias" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=CNAME - creation check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == 'www.ansible.com.\r\n' + + +- name: 'TYPE=CNAME - update address (check mode)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: docs.ansible.com, type: CNAME} + register: cmd_result + check_mode: yes + +- name: 'TYPE=CNAME - update address get results (check mode)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType CNAME -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty HostNameAlias" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=CNAME - update address check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'www.ansible.com.\r\n' + +- name: 'TYPE=CNAME - update address' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: docs.ansible.com, type: CNAME} + register: cmd_result + +- name: 'TYPE=CNAME - update address get results' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType CNAME -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty HostNameAlias" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=CNAME - update address check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'docs.ansible.com.\r\n' + +- name: 'TYPE=CNAME - update address (idempotent)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: docs.ansible.com, type: CNAME} + register: cmd_result + +- name: 'TYPE=CNAME - update address get results (idempotent)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType CNAME -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty HostNameAlias" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=CNAME - update address check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == 'docs.ansible.com.\r\n' + + +- name: 'TYPE=CNAME - update TTL (check mode)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: docs.ansible.com, ttl: 7200, type: CNAME} + register: cmd_result + check_mode: yes + +- name: 'TYPE=CNAME - update TTL get results (check mode)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType CNAME -Node -ErrorAction:Ignore | Select -ExpandProperty TimeToLive | Select -ExpandProperty TotalSeconds" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=CNAME - update TTL check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '3600\r\n' + +- name: 'TYPE=CNAME - update TTL' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: docs.ansible.com, ttl: 7200, type: CNAME} + register: cmd_result + +- name: 'TYPE=CNAME - update TTL get results' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType CNAME -Node -ErrorAction:Ignore | Select -ExpandProperty TimeToLive | Select -ExpandProperty TotalSeconds" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=CNAME - update TTL check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '7200\r\n' + +- name: 'TYPE=CNAME - update TTL (idempotent)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: docs.ansible.com, ttl: 7200, type: CNAME} + register: cmd_result + +- name: 'TYPE=CNAME - update TTL get results (idempotent)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType CNAME -Node -ErrorAction:Ignore | Select -ExpandProperty TimeToLive | Select -ExpandProperty TotalSeconds" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=CNAME - update TTL check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == '7200\r\n' + + +- name: 'TYPE=CNAME - remove record (check mode)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, type: CNAME, state: absent} + register: cmd_result + check_mode: yes + +- name: 'TYPE=CNAME - remove record get results (check mode)' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType CNAME -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=CNAME - remove record check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'exists\r\n' + +- name: 'TYPE=CNAME - remove record' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, type: CNAME, state: absent} + register: cmd_result + +- name: 'TYPE=CNAME - remove record get results' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType CNAME -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=CNAME - remove record check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'absent\r\n' + +- name: 'TYPE=CNAME - remove record (idempotent)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, type: CNAME, state: absent} + register: cmd_result + +- name: 'TYPE=CNAME - remove record get results (idempotent)' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType CNAME -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=CNAME - remove record check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == 'absent\r\n' + +- name: Issue 429 - creation + win_dns_record: + name: helloworld.test + type: CNAME + value: myserver.example.com + zone: "{{ win_dns_record_zone }}" + register: cmd_result + +- name: Issue 429 - get results + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'helloworld.test' -RRType CNAME -Node -ErrorAction:Ignore | Select-Object -ExpandProperty RecordData | Select-Object -ExpandProperty HostNameAlias" + register: cmd_result_actual + changed_when: false + +- name: Issue 429 - creation check results" + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'myserver.example.com.\r\n' diff --git a/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests-DHCID.yml b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests-DHCID.yml new file mode 100644 index 000000000..2c7323ad8 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests-DHCID.yml @@ -0,0 +1,234 @@ +- name: "TYPE=DHCID - creation (check mode)" + win_dns_record: + zone: "{{ win_dns_record_zone }}" + name: testdhcid + value: "AAIBY2/AuCccgoJbsaxcQc9TUapptP69lOjxfNuVAA2kjEA=" + type: DHCID + register: cmd_result + check_mode: yes + +- name: "TYPE=DHCID - creation get results (check mode)" + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'testdhcid' -RRType DHCID -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: "TYPE=DHCID - creation check results (check mode)" + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'absent\r\n' + +- name: "TYPE=DHCID - creation" + win_dns_record: + zone: "{{ win_dns_record_zone }}" + name: testdhcid + value: "AAIBY2/AuCccgoJbsaxcQc9TUapptP69lOjxfNuVAA2kjEA=" + type: DHCID + register: cmd_result + +- name: "TYPE=DHCID - creation get results" + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'testdhcid' -RRType DHCID -Node -ErrorAction:Ignore | Select-Object -ExpandProperty RecordData | Select-Object -ExpandProperty DhcId" + register: cmd_result_actual + changed_when: false + +- name: "TYPE=DHCID - creation check results" + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'AAIBY2/AuCccgoJbsaxcQc9TUapptP69lOjxfNuVAA2kjEA=\r\n' + +- name: "TYPE=DHCID - creation (idempotent)" + win_dns_record: + zone: "{{ win_dns_record_zone }}" + name: testdhcid + value: "AAIBY2/AuCccgoJbsaxcQc9TUapptP69lOjxfNuVAA2kjEA=" + type: DHCID + register: cmd_result + +- name: "TYPE=DHCID - creation get results (idempotent)" + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'testdhcid' -RRType DHCID -Node -ErrorAction:Ignore | Select-Object -ExpandProperty RecordData | Select-Object -ExpandProperty DhcId" + register: cmd_result_actual + changed_when: false + +- name: "TYPE=DHCID - creation check results (idempotent)" + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == 'AAIBY2/AuCccgoJbsaxcQc9TUapptP69lOjxfNuVAA2kjEA=\r\n' + +- name: "TYPE=DHCID - update value (check mode)" + win_dns_record: + zone: "{{ win_dns_record_zone }}" + name: testdhcid + value: "AAEBOSD+XR3Os/0LozeXVqcNc7FwCfQdWL3b/NaiUDlW2No=" + type: DHCID + register: cmd_result + check_mode: yes + +- name: "TYPE=DHCID - update value get results (check mode)" + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'testdhcid' -RRType DHCID -Node -ErrorAction:Ignore | Select-Object -ExpandProperty RecordData | Select-Object -ExpandProperty DhcId" + register: cmd_result_actual + changed_when: false + +- name: "TYPE=DHCID - update value check results (check mode)" + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'AAIBY2/AuCccgoJbsaxcQc9TUapptP69lOjxfNuVAA2kjEA=\r\n' + +- name: "TYPE=DHCID - update value" + win_dns_record: + zone: "{{ win_dns_record_zone }}" + name: testdhcid + value: "AAEBOSD+XR3Os/0LozeXVqcNc7FwCfQdWL3b/NaiUDlW2No=" + type: DHCID + register: cmd_result + +- name: "TYPE=DHCID - update value get results" + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'testdhcid' -RRType DHCID -Node -ErrorAction:Ignore | Select-Object -ExpandProperty RecordData | Select-Object -ExpandProperty DhcId" + register: cmd_result_actual + changed_when: false + +- name: "TYPE=DHCID - update value check results" + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'AAEBOSD+XR3Os/0LozeXVqcNc7FwCfQdWL3b/NaiUDlW2No=\r\n' + +- name: "TYPE=DHCID - update value (idempotent)" + win_dns_record: + zone: "{{ win_dns_record_zone }}" + name: testdhcid + value: "AAEBOSD+XR3Os/0LozeXVqcNc7FwCfQdWL3b/NaiUDlW2No=" + type: DHCID + register: cmd_result + +- name: "TYPE=DHCID - update value get results (idempotent)" + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'testdhcid' -RRType DHCID -Node -ErrorAction:Ignore | Select-Object -ExpandProperty RecordData | Select-Object -ExpandProperty DhcId" + register: cmd_result_actual + changed_when: false + +- name: "TYPE=DHCID - update value check results (idempotent)" + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == 'AAEBOSD+XR3Os/0LozeXVqcNc7FwCfQdWL3b/NaiUDlW2No=\r\n' + +- name: "TYPE=DHCID - update TTL (check mode)" + win_dns_record: + zone: "{{ win_dns_record_zone }}" + name: testdhcid + value: "AAEBOSD+XR3Os/0LozeXVqcNc7FwCfQdWL3b/NaiUDlW2No=" + ttl: 7200 + type: DHCID + register: cmd_result + check_mode: yes + +- name: "TYPE=DHCID - update TTL get results (check mode)" + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'testdhcid' -RRType DHCID -Node -ErrorAction:Ignore | Select-Object -ExpandProperty TimeToLive | Select-Object -ExpandProperty TotalSeconds" + register: cmd_result_actual + changed_when: false + +- name: "TYPE=DHCID - update TTL check results (check mode)" + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '3600\r\n' + +- name: "TYPE=DHCID - update TTL" + win_dns_record: + zone: "{{ win_dns_record_zone }}" + name: testdhcid + value: "AAEBOSD+XR3Os/0LozeXVqcNc7FwCfQdWL3b/NaiUDlW2No=" + ttl: 7200 + type: DHCID + register: cmd_result + +- name: "TYPE=DHCID - update TTL get results" + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'testdhcid' -RRType DHCID -Node -ErrorAction:Ignore | Select-Object -ExpandProperty TimeToLive | Select-Object -ExpandProperty TotalSeconds" + register: cmd_result_actual + changed_when: false + +- name: "TYPE=DHCID - update TTL check results" + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '7200\r\n' + +- name: "TYPE=DHCID - update TTL (idempotent)" + win_dns_record: + zone: "{{ win_dns_record_zone }}" + name: testdhcid + value: "AAEBOSD+XR3Os/0LozeXVqcNc7FwCfQdWL3b/NaiUDlW2No=" + ttl: 7200 + type: DHCID + register: cmd_result + +- name: "TYPE=DHCID - update TTL get results (idempotent)" + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'testdhcid' -RRType DHCID -Node -ErrorAction:Ignore | Select-Object -ExpandProperty TimeToLive | Select-Object -ExpandProperty TotalSeconds" + register: cmd_result_actual + changed_when: false + +- name: "TYPE=DHCID - update TTL check results (idempotent)" + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == '7200\r\n' + +- name: "TYPE=DHCID - remove record (check mode)" + win_dns_record: + zone: "{{ win_dns_record_zone }}" + name: testdhcid + type: DHCID + state: absent + register: cmd_result + check_mode: yes + +- name: "TYPE=DHCID - remove record get results (check mode)" + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'testdhcid' -RRType DHCID -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: "TYPE=DHCID - remove record check results (check mode)" + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'exists\r\n' + +- name: "TYPE=DHCID - remove record" + win_dns_record: + zone: "{{ win_dns_record_zone }}" + name: testdhcid + type: DHCID + state: absent + register: cmd_result + +- name: "TYPE=DHCID - remove record get results" + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'testdhcid' -RRType DHCID -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: "TYPE=DHCID - remove record check results" + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'absent\r\n' + +- name: "TYPE=DHCID - remove record (idempotent)" + win_dns_record: + zone: "{{ win_dns_record_zone }}" + name: testdhcid + type: DHCID + state: absent + register: cmd_result + +- name: "TYPE=DHCID - remove record get results (idempotent)" + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'testdhcid' -RRType DHCID -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: "TYPE=DHCID - remove record check results (idempotent)" + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == 'absent\r\n' diff --git a/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests-NS.yml b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests-NS.yml new file mode 100644 index 000000000..23a41da9c --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests-NS.yml @@ -0,0 +1,277 @@ +- name: 'TYPE=NS - creation (check mode)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: z, value: ansible-mirror.example.com, type: NS} + register: cmd_result + check_mode: yes + +- name: 'TYPE=NS - creation get results (check mode)' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'z' -RRType NS -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=NS - creation check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'absent\r\n' + +- name: 'TYPE=NS- creation' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: z, value: ansible-mirror.example.com, type: NS} + register: cmd_result + +- name: 'TYPE=NS - creation get results' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'z' -RRType NS -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty NameServer" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=NS - creation check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'ansible-mirror.example.com.\r\n' + +- name: 'TYPE=NS - creation (idempotent)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: z, value: ansible-mirror.example.com, type: NS} + register: cmd_result + +- name: 'TYPE=NS - creation get results (idempotent)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'z' -RRType NS -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty NameServer" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=NS - creation check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == 'ansible-mirror.example.com.\r\n' + + +- name: 'TYPE=NS - update address (check mode)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: z, value: ansible-altmirror.example.com, type: NS} + register: cmd_result + check_mode: yes + +- name: 'TYPE=NS - update address get results (check mode)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'z' -RRType NS -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty NameServer" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=NS - update address check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'ansible-mirror.example.com.\r\n' + +- name: 'TYPE=NS - update address' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: z, value: ansible-altmirror.example.com, type: NS} + register: cmd_result + +- name: 'TYPE=NS - update address get results' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'z' -RRType NS -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty NameServer" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=NS - update address check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'ansible-altmirror.example.com.\r\n' + +- name: 'TYPE=NS - update address (idempotent)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: z, value: ansible-altmirror.example.com, type: NS} + register: cmd_result + +- name: 'TYPE=NS - update address get results (idempotent)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'z' -RRType NS -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty NameServer" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=NS - update address check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == 'ansible-altmirror.example.com.\r\n' + + +- name: 'TYPE=NS - update TTL (check mode)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: z, value: ansible-altmirror.example.com, ttl: 7200, type: NS} + register: cmd_result + check_mode: yes + +- name: 'TYPE=NS - update TTL get results (check mode)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'z' -RRType NS -Node -ErrorAction:Ignore | Select -ExpandProperty TimeToLive | Select -ExpandProperty TotalSeconds" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=NS - update TTL check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '3600\r\n' + +- name: 'TYPE=NS - update TTL' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: z, value: ansible-altmirror.example.com, ttl: 7200, type: NS} + register: cmd_result + +- name: 'TYPE=NS - update TTL get results' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'z' -RRType NS -Node -ErrorAction:Ignore | Select -ExpandProperty TimeToLive | Select -ExpandProperty TotalSeconds" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=NS - update TTL check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '7200\r\n' + +- name: 'TYPE=NS - update TTL (idempotent)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: z, value: ansible-altmirror.example.com, ttl: 7200, type: NS} + register: cmd_result + +- name: 'TYPE=NS - update TTL get results (idempotent)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'z' -RRType NS -Node -ErrorAction:Ignore | Select -ExpandProperty TimeToLive | Select -ExpandProperty TotalSeconds" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=NS - update TTL check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == '7200\r\n' + +- name: 'TYPE=NS - remove record (check mode)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: z, type: NS, state: absent} + register: cmd_result + check_mode: yes + +- name: 'TYPE=NS - remove record get results (check mode)' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'z' -RRType NS -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=NS - remove record check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'exists\r\n' + +- name: 'TYPE=NS - remove record' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: z, type: NS, state: absent} + register: cmd_result + +- name: 'TYPE=NS - remove record get results' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'z' -RRType NS -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=NS - remove record check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'absent\r\n' + +- name: 'TYPE=NS - remove record (idempotent)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: z, type: NS, state: absent} + register: cmd_result + +- name: 'TYPE=NS - remove record get results (idempotent)' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'z' -RRType NS -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=NS - remove record check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == 'absent\r\n' + +- name: 'TYPE=NS - creation with multiple values (check mode)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: z, value: [ansible-mirror.example.com, ansible-altmirror.example.com, ansiblull-mirror.example.com], type: NS} + register: cmd_result + check_mode: yes + +- name: 'TYPE=NS - creation get results (check mode)' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'z' -RRType NS -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=NS - creation check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'absent\r\n' + +- name: 'TYPE=NS - creation with multiple values' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: z, value: [ansible-mirror.example.com, ansible-altmirror.example.com, ansiblull-mirror.example.com], type: NS} + register: cmd_result + +- name: 'TYPE=NS - creation get results' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'z' -RRType NS -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty NameServer" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=NS - creation check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'ansible-mirror.example.com.\r\nansible-altmirror.example.com.\r\nansiblull-mirror.example.com.\r\n' + +- name: 'TYPE=NS - creation with multiple values (idempotent)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: z, value: [ansible-mirror.example.com, ansible-altmirror.example.com, ansiblull-mirror.example.com], type: NS} + register: cmd_result + +- name: 'TYPE=NS - creation get results (idempotent)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'z' -RRType NS -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty NameServer" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=NS - creation check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == 'ansible-mirror.example.com.\r\nansible-altmirror.example.com.\r\nansiblull-mirror.example.com.\r\n' + +- name: 'TYPE=NS - remove record (check mode)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: z, type: NS, state: absent} + register: cmd_result + check_mode: yes + +- name: 'TYPE=NS - remove record get results (check mode)' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'z' -RRType NS -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=NS - remove record check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'exists\r\n' + +- name: 'TYPE=NS - remove record' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: z, type: NS, state: absent} + register: cmd_result + +- name: 'TYPE=NS - remove record get results' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'z' -RRType NS -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=NS - remove record check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'absent\r\n' + +- name: 'TYPE=NS - remove record (idempotent)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: z, type: NS, state: absent} + register: cmd_result + +- name: 'TYPE=NS - remove record get results (idempotent)' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'z' -RRType NS -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=NS - remove record check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == 'absent\r\n'
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests-PTR.yml b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests-PTR.yml new file mode 100644 index 000000000..796846f23 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests-PTR.yml @@ -0,0 +1,186 @@ +- name: 'TYPE=PTR - creation (check mode)' + win_dns_record: {zone: '{{ win_dns_record_revzone }}', name: 7, value: ansible-mirror.example.com, type: PTR} + register: cmd_result + check_mode: yes + +- name: 'TYPE=PTR - creation get results (check mode)' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_revzone }}' -Name '7' -RRType PTR -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=PTR - creation check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'absent\r\n' + +- name: 'TYPE=PTR - creation' + win_dns_record: {zone: '{{ win_dns_record_revzone }}', name: 7, value: ansible-mirror.example.com, type: PTR} + register: cmd_result + +- name: 'TYPE=PTR - creation get results' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_revzone }}' -Name '7' -RRType PTR -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty PtrDomainName" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=PTR - creation check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'ansible-mirror.example.com.\r\n' + +- name: 'TYPE=PTR - creation (idempotent)' + win_dns_record: {zone: '{{ win_dns_record_revzone }}', name: 7, value: ansible-mirror.example.com, type: PTR} + register: cmd_result + +- name: 'TYPE=PTR - creation get results (idempotent)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_revzone }}' -Name '7' -RRType PTR -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty PtrDomainName" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=PTR - creation check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == 'ansible-mirror.example.com.\r\n' + + +- name: 'TYPE=PTR - update address (check mode)' + win_dns_record: {zone: '{{ win_dns_record_revzone }}', name: 7, value: ansible-altmirror.example.com, type: PTR} + register: cmd_result + check_mode: yes + +- name: 'TYPE=PTR - update address get results (check mode)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_revzone }}' -Name '7' -RRType PTR -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty PtrDomainName" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=PTR - update address check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'ansible-mirror.example.com.\r\n' + +- name: 'TYPE=PTR - update address' + win_dns_record: {zone: '{{ win_dns_record_revzone }}', name: 7, value: ansible-altmirror.example.com, type: PTR} + register: cmd_result + +- name: 'TYPE=PTR - update address get results' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_revzone }}' -Name '7' -RRType PTR -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty PtrDomainName" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=PTR - update address check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'ansible-altmirror.example.com.\r\n' + +- name: 'TYPE=PTR - update address (idempotent)' + win_dns_record: {zone: '{{ win_dns_record_revzone }}', name: 7, value: ansible-altmirror.example.com, type: PTR} + register: cmd_result + +- name: 'TYPE=PTR - update address get results (idempotent)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_revzone }}' -Name '7' -RRType PTR -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty PtrDomainName" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=PTR - update address check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == 'ansible-altmirror.example.com.\r\n' + + +- name: 'TYPE=PTR - update TTL (check mode)' + win_dns_record: {zone: '{{ win_dns_record_revzone }}', name: 7, value: ansible-altmirror.example.com, ttl: 7200, type: PTR} + register: cmd_result + check_mode: yes + +- name: 'TYPE=PTR - update TTL get results (check mode)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_revzone }}' -Name '7' -RRType PTR -Node -ErrorAction:Ignore | Select -ExpandProperty TimeToLive | Select -ExpandProperty TotalSeconds" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=PTR - update TTL check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '3600\r\n' + +- name: 'TYPE=PTR - update TTL' + win_dns_record: {zone: '{{ win_dns_record_revzone }}', name: 7, value: ansible-altmirror.example.com, ttl: 7200, type: PTR} + register: cmd_result + +- name: 'TYPE=PTR - update TTL get results' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_revzone }}' -Name '7' -RRType PTR -Node -ErrorAction:Ignore | Select -ExpandProperty TimeToLive | Select -ExpandProperty TotalSeconds" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=PTR - update TTL check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '7200\r\n' + +- name: 'TYPE=PTR - update TTL (idempotent)' + win_dns_record: {zone: '{{ win_dns_record_revzone }}', name: 7, value: ansible-altmirror.example.com, ttl: 7200, type: PTR} + register: cmd_result + +- name: 'TYPE=PTR - update TTL get results (idempotent)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_revzone }}' -Name '7' -RRType PTR -Node -ErrorAction:Ignore | Select -ExpandProperty TimeToLive | Select -ExpandProperty TotalSeconds" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=PTR - update TTL check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == '7200\r\n' + + +- name: 'TYPE=PTR - remove record (check mode)' + win_dns_record: {zone: '{{ win_dns_record_revzone }}', name: 7, type: PTR, state: absent} + register: cmd_result + check_mode: yes + +- name: 'TYPE=PTR - remove record get results (check mode)' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_revzone }}' -Name '7' -RRType PTR -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=PTR - remove record check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'exists\r\n' + +- name: 'TYPE=PTR - remove record' + win_dns_record: {zone: '{{ win_dns_record_revzone }}', name: 7, type: PTR, state: absent} + register: cmd_result + +- name: 'TYPE=PTR - remove record get results' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_revzone }}' -Name '7' -RRType PTR -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=PTR - remove record check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'absent\r\n' + +- name: 'TYPE=PTR - remove record (idempotent)' + win_dns_record: {zone: '{{ win_dns_record_revzone }}', name: 7, type: PTR, state: absent} + register: cmd_result + +- name: 'TYPE=PTR - remove record get results (idempotent)' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_revzone }}' -Name '7' -RRType PTR -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=PTR - remove record check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == 'absent\r\n' diff --git a/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests-SRV.yml b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests-SRV.yml new file mode 100644 index 000000000..bad563565 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests-SRV.yml @@ -0,0 +1,321 @@ +- name: 'TYPE=SRV - creation (check mode)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: ansible.example.com, weight: 5, priority: 2, port: 755, type: SRV} + register: cmd_result + check_mode: yes + +- name: 'TYPE=SRV - creation get results (check mode)' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType SRV -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=SRV - creation check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'absent\r\n' + +- name: 'TYPE=SRV - creation' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: ansible.example.com, weight: 5, priority: 2, port: 755, type: SRV} + register: cmd_result + +- name: 'TYPE=SRV - creation get results' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType SRV -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty DomainName" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=SRV - creation check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'ansible.example.com.\r\n' + +- name: 'TYPE=SRV - creation (idempotent)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: ansible.example.com, weight: 5, priority: 2, port: 755, type: SRV} + register: cmd_result + +- name: 'TYPE=SRV - creation get results (idempotent)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType SRV -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty DomainName" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=SRV - creation check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == 'ansible.example.com.\r\n' + +- name: 'TYPE=SRV - update address (check mode)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: altansible.example.com, weight: 5, priority: 2, port: 755, type: SRV} + register: cmd_result + check_mode: yes + +- name: 'TYPE=SRV - update address get results (check mode)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType SRV -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty DomainName" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=SRV - update address check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'ansible.example.com.\r\n' + +- name: 'TYPE=SRV - update address' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: altansible.example.com, weight: 5, priority: 2, port: 755, type: SRV} + register: cmd_result + +- name: 'TYPE=SRV - update address get results' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType SRV -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty DomainName" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=SRV - update address check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'altansible.example.com.\r\n' + +- name: 'TYPE=SRV - update address (idempotent)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: altansible.example.com, weight: 5, priority: 2, port: 755, type: SRV} + register: cmd_result + +- name: 'TYPE=SRV - update address get results (idempotent)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType SRV -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty DomainName" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=SRV - update address check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == 'altansible.example.com.\r\n' + +- name: 'TYPE=SRV - update TTL (check mode)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: altansible.example.com, weight: 5, priority: 2, port: 755, ttl: 7200, type: SRV} + register: cmd_result + check_mode: yes + +- name: 'TYPE=SRV - update TTL get results (check mode)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType SRV -Node -ErrorAction:Ignore | Select -ExpandProperty TimeToLive | Select -ExpandProperty TotalSeconds" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=SRV - update TTL check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '3600\r\n' + +- name: 'TYPE=SRV - update TTL' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: altansible.example.com, weight: 5, priority: 2, port: 755, ttl: 7200, type: SRV} + register: cmd_result + +- name: 'TYPE=SRV - update TTL get results' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType SRV -Node -ErrorAction:Ignore | Select -ExpandProperty TimeToLive | Select -ExpandProperty TotalSeconds" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=SRV - update TTL check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '7200\r\n' + +- name: 'TYPE=SRV - update TTL (idempotent)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: altansible.example.com, weight: 5, priority: 2, port: 755, ttl: 7200, type: SRV} + register: cmd_result + +- name: 'TYPE=SRV - update TTL get results (idempotent)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType SRV -Node -ErrorAction:Ignore | Select -ExpandProperty TimeToLive | Select -ExpandProperty TotalSeconds" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=SRV - update TTL check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == '7200\r\n' + +- name: 'TYPE=SRV - update weight (check mode)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: altansible.example.com, weight: 1, priority: 2, port: 755, type: SRV} + register: cmd_result + check_mode: yes + +- name: 'TYPE=SRV - update weight get results (check mode)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType SRV -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty Weight" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=SRV - update weight check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '5\r\n' + +- name: 'TYPE=SRV - update weight' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, weight: 1, priority: 2, port: 755, value: altansible.example.com, type: SRV} + register: cmd_result + +- name: 'TYPE=SRV - update weight get results' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType SRV -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty Weight" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=SRV - update weight check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '1\r\n' + +- name: 'TYPE=SRV - update weight (idempotent)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: altansible.example.com, weight: 1, priority: 2, port: 755, type: SRV} + register: cmd_result + +- name: 'TYPE=SRV - update weight get results (idempotent)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType SRV -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty Weight" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=SRV - update weight check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == '1\r\n' + +- name: 'TYPE=SRV - update port (check mode)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: altansible.example.com, weight: 1, priority: 2, port: 355, type: SRV} + register: cmd_result + check_mode: yes + +- name: 'TYPE=SRV - update port get results (check mode)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType SRV -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty Port" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=SRV - update port check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '755\r\n' + +- name: 'TYPE=SRV - update port' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, weight: 1, priority: 2, port: 355, value: altansible.example.com, type: SRV} + register: cmd_result + +- name: 'TYPE=SRV - update port get results' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType SRV -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty Port" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=SRV - update port check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '355\r\n' + +- name: 'TYPE=SRV - update port (idempotent)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: altansible.example.com, weight: 1, priority: 2, port: 355, type: SRV} + register: cmd_result + +- name: 'TYPE=SRV - update port get results (idempotent)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType SRV -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty Port" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=SRV - update port check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == '355\r\n' + +- name: 'TYPE=SRV - update priority (check mode)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: altansible.example.com, weight: 1, priority: 7, port: 355, type: SRV} + register: cmd_result + check_mode: yes + +- name: 'TYPE=SRV - update priority get results (check mode)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType SRV -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty Priority" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=SRV - update priority check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '2\r\n' + +- name: 'TYPE=SRV - update priority' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, weight: 1, priority: 7, port: 355, value: altansible.example.com, type: SRV} + register: cmd_result + +- name: 'TYPE=SRV - update priority get results' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType SRV -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty Priority" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=SRV - update priority check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '7\r\n' + +- name: 'TYPE=SRV - update priority (idempotent)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, value: altansible.example.com, weight: 1, priority: 7, port: 355, type: SRV} + register: cmd_result + +- name: 'TYPE=SRV - update priority get results (idempotent)' + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType SRV -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty Priority" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=SRV - update priority check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == '7\r\n' + +- name: 'TYPE=SRV - remove record (check mode)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, weight: 1, priority: 7, port: 355, type: SRV, state: absent} + register: cmd_result + check_mode: yes + +- name: 'TYPE=SRV - remove record get results (check mode)' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType SRV -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=SRV - remove record check results (check mode)' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'exists\r\n' + +- name: 'TYPE=SRV - remove record' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, weight: 1, priority: 7, port: 355, type: SRV, state: absent} + register: cmd_result + +- name: 'TYPE=SRV - remove record get results' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType SRV -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=SRV - remove record check results' + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'absent\r\n' + +- name: 'TYPE=SRV - remove record (idempotent)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: test1, weight: 1, priority: 7, port: 355, type: SRV, state: absent} + register: cmd_result + +- name: 'TYPE=SRV - remove record get results (idempotent)' + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'test1' -RRType SRV -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: 'TYPE=SRV - remove record check results (idempotent)' + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == 'absent\r\n'
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests-TXT.yml b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests-TXT.yml new file mode 100644 index 000000000..9ca6bb7fa --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests-TXT.yml @@ -0,0 +1,234 @@ +- name: "TYPE=TXT - creation (check mode)" + win_dns_record: + zone: "{{ win_dns_record_zone }}" + name: testtxt + value: txtrecordvalue + type: TXT + register: cmd_result + check_mode: yes + +- name: "TYPE=TXT - creation get results (check mode)" + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'testtxt' -RRType TXT -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: "TYPE=TXT - creation check results (check mode)" + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'absent\r\n' + +- name: "TYPE=TXT - creation" + win_dns_record: + zone: "{{ win_dns_record_zone }}" + name: testtxt + value: txtrecordvalue + type: TXT + register: cmd_result + +- name: "TYPE=TXT - creation get results" + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'testtxt' -RRType TXT -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty DescriptiveText" + register: cmd_result_actual + changed_when: false + +- name: "TYPE=TXT - creation check results" + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'txtrecordvalue\r\n' + +- name: "TYPE=TXT - creation (idempotent)" + win_dns_record: + zone: "{{ win_dns_record_zone }}" + name: testtxt + value: txtrecordvalue + type: TXT + register: cmd_result + +- name: "TYPE=TXT - creation get results (idempotent)" + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'testtxt' -RRType TXT -Node -ErrorAction:Ignore | Select -ExpandProperty RecordData | Select -ExpandProperty DescriptiveText" + register: cmd_result_actual + changed_when: false + +- name: "TYPE=TXT - creation check results (idempotent)" + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == 'txtrecordvalue\r\n' + +- name: "TYPE=TXT - update value (check mode)" + win_dns_record: + zone: "{{ win_dns_record_zone }}" + name: testtxt + value: updated txt record value + type: TXT + register: cmd_result + check_mode: yes + +- name: "TYPE=TXT - update value get results (check mode)" + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'testtxt' -RRType TXT -Node -ErrorAction:Ignore | Select-Object -ExpandProperty RecordData | Select-Object -ExpandProperty DescriptiveText" + register: cmd_result_actual + changed_when: false + +- name: "TYPE=TXT - update value check results (check mode)" + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'txtrecordvalue\r\n' + +- name: "TYPE=TXT - update value" + win_dns_record: + zone: "{{ win_dns_record_zone }}" + name: testtxt + value: updated txt record value + type: TXT + register: cmd_result + +- name: "TYPE=TXT - update value get results" + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'testtxt' -RRType TXT -Node -ErrorAction:Ignore | Select-Object -ExpandProperty RecordData | Select-Object -ExpandProperty DescriptiveText" + register: cmd_result_actual + changed_when: false + +- name: "TYPE=TXT - update value check results" + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'updated txt record value\r\n' + +- name: "TYPE=TXT - update value (idempotent)" + win_dns_record: + zone: "{{ win_dns_record_zone }}" + name: testtxt + value: updated txt record value + type: TXT + register: cmd_result + +- name: "TYPE=TXT - update value get results (idempotent)" + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'testtxt' -RRType TXT -Node -ErrorAction:Ignore | Select-Object -ExpandProperty RecordData | Select-Object -ExpandProperty DescriptiveText" + register: cmd_result_actual + changed_when: false + +- name: "TYPE=TXT - update value check results (idempotent)" + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == 'updated txt record value\r\n' + +- name: "TYPE=TXT - update TTL (check mode)" + win_dns_record: + zone: "{{ win_dns_record_zone }}" + name: testtxt + value: updated txt record value + ttl: 7200 + type: TXT + register: cmd_result + check_mode: true + +- name: "TYPE=TXT - update TTL get results (check mode)" + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'testtxt' -RRType TXT -Node -ErrorAction:Ignore | Select-Object -ExpandProperty TimeToLive | Select-Object -ExpandProperty TotalSeconds" + register: cmd_result_actual + changed_when: false + +- name: "TYPE=TXT - update TTL check results (check mode)" + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '3600\r\n' + +- name: "TYPE=TXT - update TTL" + win_dns_record: + zone: "{{ win_dns_record_zone }}" + name: testtxt + value: updated txt record value + ttl: 7200 + type: TXT + register: cmd_result + +- name: "TYPE=TXT - update TTL get results" + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'testtxt' -RRType TXT -Node -ErrorAction:Ignore | Select-Object -ExpandProperty TimeToLive | Select-Object -ExpandProperty TotalSeconds" + register: cmd_result_actual + changed_when: false + +- name: "TYPE=TXT - update TTL check results" + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == '7200\r\n' + +- name: "TYPE=TXT - update TTL (idempotent)" + win_dns_record: + zone: "{{ win_dns_record_zone }}" + name: testtxt + value: updated txt record value + ttl: 7200 + type: TXT + register: cmd_result + +- name: "TYPE=TXT - update TTL get results (idempotent)" + ansible.windows.win_command: powershell.exe "Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'testtxt' -RRType TXT -Node -ErrorAction:Ignore | Select-Object -ExpandProperty TimeToLive | Select-Object -ExpandProperty TotalSeconds" + register: cmd_result_actual + changed_when: false + +- name: "TYPE=TXT - update TTL check results (idempotent)" + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == '7200\r\n' + +- name: "TYPE=TXT - remove record (check mode)" + win_dns_record: + zone: "{{ win_dns_record_zone }}" + name: testtxt + type: TXT + state: absent + register: cmd_result + check_mode: yes + +- name: "TYPE=TXT - remove record get results (check mode)" + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'testtxt' -RRType TXT -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: "TYPE=TXT - remove record check results (check mode)" + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'exists\r\n' + +- name: "TYPE=TXT - remove record" + win_dns_record: + zone: "{{ win_dns_record_zone }}" + name: testtxt + type: TXT + state: absent + register: cmd_result + +- name: "TYPE=TXT - remove record get results" + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'testtxt' -RRType TXT -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: "TYPE=TXT - remove record check results" + assert: + that: + - cmd_result is changed + - cmd_result_actual.stdout == 'absent\r\n' + +- name: "TYPE=TXT - remove record (idempotent)" + win_dns_record: + zone: "{{ win_dns_record_zone }}" + name: testtxt + type: TXT + state: absent + register: cmd_result + +- name: "TYPE=TXT - remove record get results (idempotent)" + ansible.windows.win_command: powershell.exe "If (Get-DnsServerResourceRecord -ZoneName '{{ win_dns_record_zone }}' -Name 'testtxt' -RRType TXT -Node -ErrorAction:Ignore) { 'exists' } else { 'absent' }" + register: cmd_result_actual + changed_when: false + +- name: "TYPE=TXT - remove record check results (idempotent)" + assert: + that: + - cmd_result is not changed + - cmd_result_actual.stdout == 'absent\r\n' diff --git a/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests-diff.yml b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests-diff.yml new file mode 100644 index 000000000..f5adaf369 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests-diff.yml @@ -0,0 +1,63 @@ +# Diff tests are present because those records have to be created MANUALLY by +# the win_dns_record module when in check mode, as there is otherwise no way in +# Windows DNS to *simulate* a record or change. + + +- name: 'Diff test - creation (check mode)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: diff_host, value: 1.2.3.4, type: A} + register: create_check + check_mode: yes + diff: yes + +- name: 'Diff test - creation' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: diff_host, value: 1.2.3.4, type: A} + register: create_do + diff: yes + +- name: 'Diff test - creation check results' + assert: + that: + - create_check.diff.before == create_do.diff.before + - create_check.diff.before == '' + - create_check.diff.after == create_do.diff.after + - create_check.diff.after == "[{{ win_dns_record_zone }}] diff_host 3600 IN A 1.2.3.4\n" + + +- name: 'Diff test - update TTL (check mode)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: diff_host, value: 1.2.3.4, type: A, ttl: 7200} + register: update_check + check_mode: yes + diff: yes + +- name: 'Diff test - update TTL' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: diff_host, value: 1.2.3.4, type: A, ttl: 7200} + register: update_do + diff: yes + +- name: 'Diff test - update TTL check results' + assert: + that: + - update_check.diff.before == update_do.diff.before + - update_check.diff.before == "[{{ win_dns_record_zone }}] diff_host 3600 IN A 1.2.3.4\n" + - update_check.diff.after == update_do.diff.after + - update_check.diff.after == "[{{ win_dns_record_zone }}] diff_host 7200 IN A 1.2.3.4\n" + + +- name: 'Diff test - deletion (check mode)' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: diff_host, type: A, state: absent} + register: delete_check + check_mode: yes + diff: yes + +- name: 'Diff test - deletion' + win_dns_record: {zone: '{{ win_dns_record_zone }}', name: diff_host, type: A, state: absent} + register: delete_do + diff: yes + +- name: 'Diff test - deletion check results' + assert: + that: + - delete_check.diff.before == delete_do.diff.before + - delete_check.diff.before == "[{{ win_dns_record_zone }}] diff_host 7200 IN A 1.2.3.4\n" + - delete_check.diff.after == delete_do.diff.after + - delete_check.diff.after == '' diff --git a/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests.yml new file mode 100644 index 000000000..bdf40799b --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_dns_record/tasks/tests.yml @@ -0,0 +1,36 @@ +- name: ensure DNS services are installed + ansible.windows.win_feature: + name: DNS + state: present + register: dns_install + +- name: reboot server if needed + ansible.windows.win_reboot: + when: dns_install.reboot_required + +- name: Clean slate + import_tasks: clean.yml + vars: + fail_on_missing: false + +- block: + - name: Create the forward zone + ansible.windows.win_shell: Add-DnsServerPrimaryZone -Name '{{ win_dns_record_zone }}' -ZoneFile '{{ win_dns_record_zone}}.dns' + - name: Create the reverse zone + ansible.windows.win_shell: Add-DnsServerPrimaryZone -NetworkID '{{ win_dns_record_revzone_network }}' -ZoneFile '{{ win_dns_record_revzone}}.dns' + + - import_tasks: tests-A.yml + - import_tasks: tests-AAAA.yml + - import_tasks: tests-NS.yml + - import_tasks: tests-SRV.yml + - import_tasks: tests-CNAME.yml + - import_tasks: tests-DHCID.yml + - import_tasks: tests-PTR.yml + - import_tasks: tests-TXT.yml + - import_tasks: tests-diff.yml + + always: + - name: Clean slate + import_tasks: clean.yml + vars: + fail_on_missing: true diff --git a/ansible_collections/community/windows/tests/integration/targets/win_dns_zone/aliases b/ansible_collections/community/windows/tests/integration/targets/win_dns_zone/aliases new file mode 100644 index 000000000..df7c2d121 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_dns_zone/aliases @@ -0,0 +1,2 @@ +shippable/windows/group1 +skip/windows/2012
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_dns_zone/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_dns_zone/defaults/main.yml new file mode 100644 index 000000000..73afd8e1b --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_dns_zone/defaults/main.yml @@ -0,0 +1,7 @@ +--- +# Defines the test to run, possible values: 'standalone' or 'activedirectory' +# 'standalone' installs a mock Windows DNS server and runs tests that only +# include file backed DNS zones. +# 'activedirectory' installs a mock AD/DNS server and runs tests that include +# AD integrated and file backed DNS zones. +win_dns_zone_test_type: standalone
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_dns_zone/tasks/activedirectory.yml b/ansible_collections/community/windows/tests/integration/targets/win_dns_zone/tasks/activedirectory.yml new file mode 100644 index 000000000..fa75cf59e --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_dns_zone/tasks/activedirectory.yml @@ -0,0 +1,306 @@ +--- +- name: Ensure AD/DNS roles are installed + ansible.windows.win_feature: + name: + - AD-Domain-Services + - DNS + include_management_tools: true + include_sub_features: true + state: present + +- name: Ensure domain is present + ansible.windows.win_domain: + dns_domain_name: ansible.test + safe_mode_password: password123! + +- name: Reboot + ansible.windows.win_reboot: + +- name: Ensure loopback address is set to DNS + ansible.windows.win_dns_client: + adapter_names: '*' + ipv4_addresses: 127.0.0.1 + +- name: Reboot Again to Avoid DNS Bug + ansible.windows.win_reboot: + +- name: Ensure DNS zones are absent + win_dns_zone: + name: "{{ item }}" + state: absent + loop: + - jamals.euc.vmware.com + - dgemzer.euc.vmware.com + - wpinner.euc.vmware.com + - marshallb.euc.vmware.com + - basavaraju.euc.vmware.com + - virajp.euc.vmware.com + +- name: Ensure primary DNS zone is present + win_dns_zone: + name: wpinner.euc.vmware.com + replication: domain + type: primary + state: present + register: test1a + failed_when: test1a.changed == false + +- name: Ensure primary DNS zone is present (idempotence check) + win_dns_zone: + name: wpinner.euc.vmware.com + replication: domain + type: primary + state: present + register: test1 + failed_when: test1 is changed + +- name: Ensure Active Directory integrated primary zone is present with secure updates + win_dns_zone: + name: basavaraju.euc.vmware.com + type: primary + replication: forest + dynamic_update: secure + register: test9a + failed_when: test9a.changed == false + +- name: Ensure Active Directory integrated primary zone is present with secure updates (idempotence check) + win_dns_zone: + name: basavaraju.euc.vmware.com + type: primary + replication: forest + dynamic_update: secure + register: test9 + failed_when: test9 is changed + +- name: Ensure primary DNS zone is present, change replication + win_dns_zone: + name: wpinner.euc.vmware.com + replication: forest + type: primary + state: present + register: test2a + failed_when: test2a.changed == false + +- name: Ensure primary DNS zone is present, change replication (idempotence check) + win_dns_zone: + name: wpinner.euc.vmware.com + replication: forest + type: primary + state: present + register: test2 + failed_when: test2 is changed + +- name: Ensure DNS zone is absent + win_dns_zone: + name: wpinner.euc.vmware.com + state: absent + register: test3a + failed_when: test3a.changed == false + +- name: Ensure DNS zone is absent (idempotence check) + win_dns_zone: + name: wpinner.euc.vmware.com + state: absent + register: test3 + failed_when: test3 is changed + +- name: Ensure forwarder has specific DNS servers + win_dns_zone: + name: jamals.euc.vmware.com + type: forwarder + replication: forest + forwarder_timeout: 2 + dns_servers: + - 10.245.51.100 + - 10.245.51.101 + - 10.245.51.102 + register: test4a + failed_when: test4a.changed == false + +- name: Ensure forwarder has specific DNS servers (idempotence check) + win_dns_zone: + name: jamals.euc.vmware.com + type: forwarder + dns_servers: + - 10.245.51.100 + - 10.245.51.101 + - 10.245.51.102 + register: test4 + failed_when: test4 is changed + +- name: Ensure stub zone is configured + win_dns_zone: + name: dgemzer.euc.vmware.com + type: stub + replication: none + dns_servers: + - 10.19.20.1 + - 10.19.20.2 + register: test5a + failed_when: test5a.changed == false + +- name: Ensure stub zone is configured (idempotence check) + win_dns_zone: + name: dgemzer.euc.vmware.com + type: stub + replication: none + dns_servers: + - 10.19.20.1 + - 10.19.20.2 + register: test5 + failed_when: test5 is changed + +- name: Ensure forwarder zone has updated DNS servers + win_dns_zone: + name: jamals.euc.vmware.com + type: forwarder + dns_servers: + - 10.10.1.150 + - 10.10.1.151 + register: test6a + failed_when: test6a.changed == false + +- name: Ensure forwarder zone has updated DNS servers (idempotence check) + win_dns_zone: + name: jamals.euc.vmware.com + type: forwarder + dns_servers: + - 10.10.1.150 + - 10.10.1.151 + register: test6 + failed_when: test6 is changed + +- name: Ensure Active Directory integrated secondary zone is present + win_dns_zone: + name: virajp.euc.vmware.com + type: primary + replication: forest + dynamic_update: none + register: test7a + failed_when: test7a.changed == false + +- name: Ensure Active Directory integrated secondary zone is present (idempotence check) + win_dns_zone: + name: virajp.euc.vmware.com + type: primary + replication: forest + dynamic_update: none + register: test7 + failed_when: test7 is changed + +- name: Ensure file backed primary zone is present + win_dns_zone: + name: marshallb.euc.vmware.com + type: primary + replication: none + register: test8a + failed_when: test8a.changed == false + +- name: Ensure file backed primary zone is present (idempotence check) + win_dns_zone: + name: marshallb.euc.vmware.com + type: primary + replication: none + register: test8 + failed_when: test8 is changed + +- name: Ensure Active Directory integrated dynamic updates set to nonsecureandsecure + win_dns_zone: + name: basavaraju.euc.vmware.com + type: primary + dynamic_update: nonsecureandsecure + register: test10a + failed_when: test10a.changed == false + +- name: Ensure file backed primary zone has dynamic updates set to nonsecureandsecure (idempotence check) + win_dns_zone: + name: basavaraju.euc.vmware.com + type: primary + dynamic_update: nonsecureandsecure + register: test10 + failed_when: test10 is changed + +- name: Ensure zone has dynamic update set to secure and replication set to domain + win_dns_zone: + name: basavaraju.euc.vmware.com + type: primary + dynamic_update: secure + replication: domain + register: test11a + failed_when: test11a.changed == false + +- name: Ensure zone has dynamic update set to secure and replication set to domain (idempotence check) + win_dns_zone: + name: basavaraju.euc.vmware.com + type: primary + dynamic_update: secure + replication: domain + register: test11 + failed_when: test11 is changed + +- name: Ensure primary DNS zones are present (check mode) + win_dns_zone: + name: mehmoodkhap.euc.vmware.com + replication: domain + type: primary + check_mode: true + register: cm_test1 + failed_when: cm_test1 is changed + +- name: Ensure primary DNS zones replicate to forest (check mode) + win_dns_zone: + name: chall.euc.vmware.com + replication: forest + type: primary + check_mode: true + register: cm_test2 + failed_when: cm_test2 is changed + +- name: Ensure forwarder is present (check mode) + win_dns_zone: + name: nkini.euc.vmware.com + type: forwarder + dns_servers: + - 10.245.51.100 + - 10.245.51.101 + - 10.245.51.102 + check_mode: true + register: cm_test3 + failed_when: cm_test3 is changed + +- name: Ensure forwarder zone has specific DNS servers (check mode) + win_dns_zone: + name: ssanthanagopalan.euc.vmware.com + type: forwarder + dns_servers: + - 10.205.1.219 + - 10.205.1.220 + check_mode: true + register: cm_test4 + failed_when: cm_test4 is changed + +- name: Ensure Active Directory integrated secondary zone is present (check mode) + win_dns_zone: + name: rrounsaville.euc.vmware.com + type: secondary + dns_servers: + - 10.205.1.219 + - 10.205.1.220 + replication: forest + dynamic_update: none + check_mode: true + register: cm_test5 + failed_when: cm_test5 is changed + +- name: Ensure file backed stub zone is present (check mode) + win_dns_zone: + name: anup.euc.vmware.com + type: stub + dns_servers: + - 10.205.1.219 + - 10.205.1.220 + replication: none + dynamic_update: none + check_mode: true + register: cm_test6 + failed_when: cm_test6 is changed diff --git a/ansible_collections/community/windows/tests/integration/targets/win_dns_zone/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_dns_zone/tasks/main.yml new file mode 100644 index 000000000..e9bebdfcb --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_dns_zone/tasks/main.yml @@ -0,0 +1,3 @@ +--- +- name: Run Defined Test Flow + include_tasks: "{{ win_dns_zone_test_type }}.yml" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_dns_zone/tasks/standalone.yml b/ansible_collections/community/windows/tests/integration/targets/win_dns_zone/tasks/standalone.yml new file mode 100644 index 000000000..ed52cb6d4 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_dns_zone/tasks/standalone.yml @@ -0,0 +1,251 @@ +--- +- name: Ensure AD/DNS roles are installed + ansible.windows.win_feature: + name: + - DNS + include_management_tools: true + include_sub_features: true + state: present + register: ensure_addns_roles + +- name: Ensure loopback address is set to DNS + ansible.windows.win_dns_client: + adapter_names: '*' + ipv4_addresses: 127.0.0.1 + +- name: Reboot + ansible.windows.win_reboot: + when: ensure_addns_roles.reboot_required + +- name: Ensure DNS zones are absent + win_dns_zone: + name: "{{ item }}" + state: absent + loop: + - jamals.euc.vmware.com + - dgemzer.euc.vmware.com + - wpinner.euc.vmware.com + - marshallb.euc.vmware.com + - basavaraju.euc.vmware.com + - virajp.euc.vmware.com + +- name: Ensure file-backed primary DNS zone is present + win_dns_zone: + name: wpinner.euc.vmware.com + replication: none + type: primary + state: present + register: test1a + failed_when: test1a.changed == false + +- name: Ensure file-backed primary DNS zone is present (idempotence check) + win_dns_zone: + name: wpinner.euc.vmware.com + replication: none + type: primary + state: present + register: test1 + failed_when: test1 is changed + +- name: Ensure file-backed primary zone is present with secure updates, generates warning + win_dns_zone: + name: basavaraju.euc.vmware.com + type: primary + replication: none + dynamic_update: secure + register: test2a + failed_when: test2a.changed == false + +- name: Ensure file-backed primary zone is present with secure updates, generates warning (idempotence check) + win_dns_zone: + name: basavaraju.euc.vmware.com + type: primary + replication: none + dynamic_update: secure + register: test2 + failed_when: test2 is changed + +- name: Ensure DNS zone is absent + win_dns_zone: + name: wpinner.euc.vmware.com + state: absent + register: test3a + failed_when: test3a.changed == false + +- name: Ensure DNS zone is absent (idempotence check) + win_dns_zone: + name: wpinner.euc.vmware.com + state: absent + register: test3 + failed_when: test3 is changed + +- name: Ensure file-backed forwarder has specific DNS servers + win_dns_zone: + name: jamals.euc.vmware.com + type: forwarder + replication: none + forwarder_timeout: 2 + dns_servers: + - 10.245.51.100 + - 10.245.51.101 + - 10.245.51.102 + register: test4a + failed_when: test4a.changed == false + +- name: Ensure file-backed forwarder has specific DNS servers (idempotence check) + win_dns_zone: + name: jamals.euc.vmware.com + type: forwarder + replication: none + dns_servers: + - 10.245.51.100 + - 10.245.51.101 + - 10.245.51.102 + register: test4 + failed_when: test4 is changed + +- name: Ensure file-backed stub zone is configured + win_dns_zone: + name: dgemzer.euc.vmware.com + type: stub + replication: none + dns_servers: + - 10.19.20.1 + - 10.19.20.2 + register: test5a + failed_when: test5a.changed == false + +- name: Ensure file-backed stub zone is configured (idempotence check) + win_dns_zone: + name: dgemzer.euc.vmware.com + type: stub + replication: none + dns_servers: + - 10.19.20.1 + - 10.19.20.2 + register: test5 + failed_when: test5 is changed + +- name: Ensure file-backed forwarder zone has updated DNS servers + win_dns_zone: + name: jamals.euc.vmware.com + type: forwarder + replication: none + dns_servers: + - 10.10.1.150 + - 10.10.1.151 + register: test6a + failed_when: test6a.changed == false + +- name: Ensure file-backed forwarder zone has updated DNS servers (idempotence check) + win_dns_zone: + name: jamals.euc.vmware.com + type: forwarder + replication: none + dns_servers: + - 10.10.1.150 + - 10.10.1.151 + register: test6 + failed_when: test6 is changed + +- name: Ensure file backed primary zone is present + win_dns_zone: + name: marshallb.euc.vmware.com + type: primary + replication: none + register: test7a + failed_when: test7a.changed == false + +- name: Ensure file backed primary zone is present (idempotence check) + win_dns_zone: + name: marshallb.euc.vmware.com + type: primary + replication: none + register: test7 + failed_when: test7 is changed + +- name: Ensure file backed integrated dynamic updates set to none + win_dns_zone: + name: virajp.euc.vmware.com + type: primary + replication: none + dynamic_update: none + register: test8a + failed_when: test8a.changed == false + +- name: Ensure file backed primary zone has dynamic updates set to none (idempotence check) + win_dns_zone: + name: virajp.euc.vmware.com + type: primary + replication: none + dynamic_update: none + register: test8 + failed_when: test8 is changed + +- name: Start Check Mode Tests + block: + - name: Ensure primary DNS zones are present (check mode) + win_dns_zone: + name: mehmoodkhap.euc.vmware.com + replication: none + type: primary + dynamic_update: none + register: cm_test1 + failed_when: cm_test1 is changed + + - name: Ensure file-backed primary DNS zone is present (check mode) + win_dns_zone: + name: chall.euc.vmware.com + replication: none + type: primary + + register: cm_test2 + failed_when: cm_test2 is changed + + - name: Ensure file-backed forwarder is present (check mode) + win_dns_zone: + name: nkini.euc.vmware.com + replication: none + type: forwarder + dns_servers: + - 10.245.51.100 + - 10.245.51.101 + - 10.245.51.102 + register: cm_test3 + failed_when: cm_test3 is changed + + - name: Ensure file-backed forwarder zone has specific DNS servers (check mode) + win_dns_zone: + name: ssanthanagopalan.euc.vmware.com + replication: none + type: forwarder + dns_servers: + - 10.205.1.219 + - 10.205.1.220 + register: cm_test4 + failed_when: test4 is changed + + - name: Ensure file-backed integrated secondary zone is present (check mode) + win_dns_zone: + name: rrounsaville.euc.vmware.com + type: secondary + dns_servers: + - 10.205.1.219 + - 10.205.1.220 + replication: none + dynamic_update: none + register: cm_test5 + failed_when: cm_test5 is changed + + - name: Ensure file-backed stub zone is present (check mode) + win_dns_zone: + name: anup.euc.vmware.com + type: stub + dns_servers: + - 10.205.1.219 + - 10.205.1.220 + replication: none + dynamic_update: none + register: cm_test6 + failed_when: cm_test6 is changed + check_mode: true diff --git a/ansible_collections/community/windows/tests/integration/targets/win_domain_computer/aliases b/ansible_collections/community/windows/tests/integration/targets/win_domain_computer/aliases new file mode 100644 index 000000000..ad7ccf7ad --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_domain_computer/aliases @@ -0,0 +1 @@ +unsupported diff --git a/ansible_collections/community/windows/tests/integration/targets/win_domain_computer/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_domain_computer/tasks/main.yml new file mode 100644 index 000000000..b8fe35022 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_domain_computer/tasks/main.yml @@ -0,0 +1,478 @@ +# this won't run in Ansible's integration tests until we get a domain set up +# these are here if someone wants to run the module tests locally on their own +# domain. +# Requirements: +# Set the names in vars: on the command line, or set the following: +# test_domain_name: The DNS name of the domain like ansible.local +# test_ad_domain_dn: The DN of the domain like DC=ansible,DC=local +# test_ad_computer_ou: The DN of the OU where computers will be created like OU=ou1,DC=ansible,DC=local +# +# This is not a traditional role, and can't be used with ansible-test. This is a playbook. To run ensure: +# - your collections are set up and Ansible knows where to find them ($ANSIBLE_COLLECTIONS_PATHS for example) +# - your inventory contains a host where this can run, like a domain controller +# - connection keywords/options/vars are set properly to connect to the host +# - the variable win_domain_computer_testing_host contains the name of the host or the group that contains it +# +# then call this file with ansible-playbook and any extra vars or other params you need +--- +- name: run win_domain_users test + hosts: "{{ win_domain_computer_testing_host }}" + gather_facts: no + collections: + - community.windows + vars: + test_win_domain_computer_ldap_base: "{{ test_ad_domain_dn }}" + test_win_domain_computer_ou_path: "{{ test_ad_computer_ou }}" + test_win_domain_computer_name: "test_computer" + test_win_domain_domain_name: "{{ test_domain_name }}" + test_win_domain_computer_dns_hostname: "{{ test_win_domain_computer_name }}.{{ test_domain_name }}" + test_win_domain_computer_description: "{{ test_computer_description | default('description') }}" + tasks: + + - name: ensure the computer is deleted before the test + win_domain_computer: + name: '{{ test_win_domain_computer_name }}' + state: absent + tags: always + + # -------------------------------------------------------------------------- + + - name: Test computer with long name and distinct sam_account_name + tags: + - long_name + vars: + test_win_domain_computer_long_name: '{{ test_win_domain_computer_name }}_with_long_name' + test_win_domain_computer_sam_account_name: '{{ test_win_domain_computer_name }}$' + test_win_domain_computer_dns_hostname: "{{ test_win_domain_computer_long_name }}.{{ test_domain_name }}" + block: + + # ---------------------------------------------------------------------- + - name: create computer with long name and distinct sam_account_name (check mode) + win_domain_computer: + name: '{{ test_win_domain_computer_long_name }}' + dns_hostname: '{{ test_win_domain_computer_dns_hostname }}' + sam_account_name: '{{ test_win_domain_computer_sam_account_name }}' + ou: "{{ test_win_domain_computer_ou_path }}" + enabled: yes + state: present + register: create_distinct_sam_account_name + check_mode: yes + + - name: assert create computer with long name and distinct sam_account_name (check mode) + assert: + that: + - create_distinct_sam_account_name is changed + + - name: create computer with long name and distinct sam_account_name + win_domain_computer: + name: '{{ test_win_domain_computer_long_name }}' + dns_hostname: '{{ test_win_domain_computer_dns_hostname }}' + sam_account_name: '{{ test_win_domain_computer_sam_account_name }}' + ou: "{{ test_win_domain_computer_ou_path }}" + enabled: yes + state: present + register: create_distinct_sam_account_name + + - name: get actual computer with long name and distinct sam_account_name + ansible.windows.win_shell: | + Import-Module ActiveDirectory + $c = Get-ADComputer -Identity '{{ test_win_domain_computer_sam_account_name }}' -ErrorAction Stop + if ($c.Name -ne '{{ test_win_domain_computer_long_name }}') { + throw 'Wrong computer name in relation to sAMAccountName' + } + register: create_distinct_sam_account_name_check + + - name: (Idempotence) create computer with long name and distinct sam_account_name + win_domain_computer: + name: '{{ test_win_domain_computer_long_name }}' + dns_hostname: '{{ test_win_domain_computer_dns_hostname }}' + sam_account_name: '{{ test_win_domain_computer_sam_account_name }}' + ou: "{{ test_win_domain_computer_ou_path }}" + enabled: yes + state: present + register: create_distinct_sam_account_name_idempotence + check_mode: yes + + - name: (Idempotence) assert create computer with long name and distinct sam_account_name + assert: + that: + - create_distinct_sam_account_name_idempotence is not changed + + always: + - name: ensure the test computer is deleted after the test + win_domain_computer: + name: '{{ test_win_domain_computer_long_name }}' + sam_account_name: '{{ test_win_domain_computer_sam_account_name }}' + state: absent + + # ---------------------------------------------------------------------- + + - name: Test offline domain join + tags: + - djoin + vars: + test_win_domain_computer_sam_account_name: '{{ test_win_domain_computer_name }}$' + block: + - name: No file with blob return + block: + - name: Create computer with offline domain join and blob return (check mode) + win_domain_computer: + name: '{{ test_win_domain_computer_name }}' + dns_hostname: '{{ test_win_domain_computer_dns_hostname }}' + sam_account_name: '{{ test_win_domain_computer_sam_account_name }}' + ou: "{{ test_win_domain_computer_ou_path }}" + description: "{{ test_computer_description }}" + enabled: yes + state: present + offline_domain_join: output + register: odj_result + check_mode: yes + + - name: assert odj (check mode) + assert: + that: + - odj_result is changed + - odj_result.odj_blob is defined + - odj_result.odj_blob == '' + - odj_result.odj_blob_path is not defined + - odj_result.djoin is defined + - odj_result.djoin.invocation is defined + + - name: Create computer with offline domain join and blob return + win_domain_computer: + name: '{{ test_win_domain_computer_name }}' + dns_hostname: '{{ test_win_domain_computer_dns_hostname }}' + sam_account_name: '{{ test_win_domain_computer_sam_account_name }}' + ou: "{{ test_win_domain_computer_ou_path }}" + description: "{{ test_computer_description }}" + enabled: yes + state: present + offline_domain_join: output + register: odj_result + + - name: assert odj + assert: + that: + - odj_result is changed + - odj_result.odj_blob is defined + - odj_result.odj_blob != '' + - odj_result.odj_blob_path is not defined + - odj_result.djoin is defined + - odj_result.djoin.invocation is defined + - odj_result.djoin.rc is defined + - odj_result.djoin.rc == 0 + - odj_result.djoin.stdout is defined + - odj_result.djoin.stderr is defined + + - name: Create computer with offline domain join and blob return (idempotence) + win_domain_computer: + name: '{{ test_win_domain_computer_name }}' + dns_hostname: '{{ test_win_domain_computer_dns_hostname }}' + sam_account_name: '{{ test_win_domain_computer_sam_account_name }}' + ou: "{{ test_win_domain_computer_ou_path }}" + description: "{{ test_computer_description }}" + enabled: yes + state: present + offline_domain_join: output + register: odj_result + + - name: assert odj + assert: + that: + - odj_result is not changed + - odj_result.odj_blob is not defined + - odj_result.odj_blob_path is not defined + - odj_result.djoin is not defined + + always: + - name: ensure the test computer is deleted after the test + win_domain_computer: + name: '{{ test_win_domain_computer_name }}' + sam_account_name: '{{ test_win_domain_computer_sam_account_name }}' + state: absent + + + - name: File and blob return + vars: + blob_path: 'C:\Windows\Temp\blob.txt' + block: + - name: Create computer with offline domain join and blob file with return (check mode) + win_domain_computer: + name: '{{ test_win_domain_computer_name }}' + dns_hostname: '{{ test_win_domain_computer_dns_hostname }}' + sam_account_name: '{{ test_win_domain_computer_sam_account_name }}' + ou: "{{ test_win_domain_computer_ou_path }}" + description: "{{ test_computer_description }}" + enabled: yes + state: present + offline_domain_join: output + odj_blob_path: "{{ blob_path }}" + register: odj_result + check_mode: yes + + - name: assert odj (check mode) + assert: + that: + - odj_result is changed + - odj_result.odj_blob is defined + - odj_result.odj_blob == '' + - odj_result.odj_blob_path is not defined + - odj_result.djoin is defined + - odj_result.djoin.invocation is defined + + - name: Create computer with offline domain join and blob file with return + win_domain_computer: + name: '{{ test_win_domain_computer_name }}' + dns_hostname: '{{ test_win_domain_computer_dns_hostname }}' + sam_account_name: '{{ test_win_domain_computer_sam_account_name }}' + ou: "{{ test_win_domain_computer_ou_path }}" + description: "{{ test_computer_description }}" + enabled: yes + state: present + offline_domain_join: output + odj_blob_path: "{{ blob_path }}" + register: odj_result + + - name: assert odj + assert: + that: + - odj_result is changed + - odj_result.odj_blob is defined + - odj_result.odj_blob != '' + - odj_result.odj_blob_path is not defined + - odj_result.djoin is defined + - odj_result.djoin.invocation is defined + - odj_result.djoin.rc is defined + - odj_result.djoin.rc == 0 + - odj_result.djoin.stdout is defined + - odj_result.djoin.stderr is defined + + - name: Create computer with offline domain join and blob file with return (idempotence) + win_domain_computer: + name: '{{ test_win_domain_computer_name }}' + dns_hostname: '{{ test_win_domain_computer_dns_hostname }}' + sam_account_name: '{{ test_win_domain_computer_sam_account_name }}' + ou: "{{ test_win_domain_computer_ou_path }}" + description: "{{ test_computer_description }}" + enabled: yes + state: present + offline_domain_join: output + odj_blob_path: "{{ blob_path }}" + register: odj_result + + - name: assert odj + assert: + that: + - odj_result is not changed + - odj_result.odj_blob is not defined + - odj_result.odj_blob_path is not defined + - odj_result.djoin is not defined + + always: + - name: ensure the test computer is deleted after the test + win_domain_computer: + name: '{{ test_win_domain_computer_name }}' + sam_account_name: '{{ test_win_domain_computer_sam_account_name }}' + state: absent + + - name: ensure the blob file is deleted + win_shell: | + Remove-Item -LiteralPath '{{ blob_path }}' -Force -ErrorAction SilentlyContinue + exit 0 + + - name: Specified file return + vars: + blob_path: 'C:\Windows\Temp\blob.txt' + block: + - name: Create computer with offline domain join and blob file return with specified path (check mode) + win_domain_computer: + name: '{{ test_win_domain_computer_name }}' + dns_hostname: '{{ test_win_domain_computer_dns_hostname }}' + sam_account_name: '{{ test_win_domain_computer_sam_account_name }}' + ou: "{{ test_win_domain_computer_ou_path }}" + description: "{{ test_computer_description }}" + enabled: yes + state: present + offline_domain_join: path + odj_blob_path: "{{ blob_path }}" + register: odj_result + check_mode: yes + + - name: assert odj (check mode) + assert: + that: + - odj_result is changed + - odj_result.odj_blob is defined + - odj_result.odj_blob == '' + - odj_result.odj_blob_path is defined + - odj_result.odj_blob_path == blob_path + - odj_result.djoin is defined + - odj_result.djoin.invocation is defined + + - name: Create computer with offline domain join and blob file return with specified path + win_domain_computer: + name: '{{ test_win_domain_computer_name }}' + dns_hostname: '{{ test_win_domain_computer_dns_hostname }}' + sam_account_name: '{{ test_win_domain_computer_sam_account_name }}' + ou: "{{ test_win_domain_computer_ou_path }}" + description: "{{ test_computer_description }}" + enabled: yes + state: present + offline_domain_join: path + odj_blob_path: "{{ blob_path }}" + register: odj_result + + - name: assert odj + assert: + that: + - odj_result is changed + - odj_result.odj_blob is defined + - odj_result.odj_blob == '' + - odj_result.odj_blob_path is defined + - odj_result.odj_blob_path == blob_path + - odj_result.djoin is defined + - odj_result.djoin.invocation is defined + - odj_result.djoin.rc is defined + - odj_result.djoin.rc == 0 + - odj_result.djoin.stdout is defined + - odj_result.djoin.stderr is defined + + - name: Test ODJ File + ansible.windows.win_shell: | + $ErrorActionPreference = 'Stop' + $file = '{{ odj_result.odj_blob_path }}' + $content = Get-Content -LiteralPath $file -Raw -Encoding Unicode + $trimmed = $content.TrimEnd("`0") + if ($content.Length -eq $trimmed.Length) { throw 'No terminating null found' } + # try a base64 decode to validate it is the kind of data we expect + $bytes = [Convert]::FromBase64String($trimmed) + + - name: Create computer with offline domain join and blob file return with specified path (idempotence) + win_domain_computer: + name: '{{ test_win_domain_computer_name }}' + dns_hostname: '{{ test_win_domain_computer_dns_hostname }}' + sam_account_name: '{{ test_win_domain_computer_sam_account_name }}' + ou: "{{ test_win_domain_computer_ou_path }}" + description: "{{ test_computer_description }}" + enabled: yes + state: present + offline_domain_join: path + odj_blob_path: "{{ blob_path }}" + register: odj_result + + - name: assert odj + assert: + that: + - odj_result is not changed + - odj_result.odj_blob is not defined + - odj_result.odj_blob_path is not defined + - odj_result.djoin is not defined + + always: + - name: ensure the test computer is deleted after the test + win_domain_computer: + name: '{{ test_win_domain_computer_name }}' + sam_account_name: '{{ test_win_domain_computer_sam_account_name }}' + state: absent + + - name: ensure the blob file is deleted + win_shell: | + Remove-Item -LiteralPath '{{ blob_path }}' -Force -ErrorAction SilentlyContinue + exit 0 + + - name: Random file return + block: + - name: Create computer with offline domain join and random blob file return (check mode) + win_domain_computer: + name: '{{ test_win_domain_computer_name }}' + dns_hostname: '{{ test_win_domain_computer_dns_hostname }}' + sam_account_name: '{{ test_win_domain_computer_sam_account_name }}' + ou: "{{ test_win_domain_computer_ou_path }}" + description: "{{ test_computer_description }}" + enabled: yes + state: present + offline_domain_join: path + register: odj_result + check_mode: yes + + - name: assert odj (check mode) + assert: + that: + - odj_result is changed + - odj_result.odj_blob is defined + - odj_result.odj_blob == '' + - odj_result.odj_blob_path is defined + - odj_result.djoin is defined + - odj_result.djoin.invocation is defined + + - name: Create computer with offline domain join and random blob file return + win_domain_computer: + name: '{{ test_win_domain_computer_name }}' + dns_hostname: '{{ test_win_domain_computer_dns_hostname }}' + sam_account_name: '{{ test_win_domain_computer_sam_account_name }}' + ou: "{{ test_win_domain_computer_ou_path }}" + description: "{{ test_computer_description }}" + enabled: yes + state: present + offline_domain_join: path + register: odj_result + + - name: assert odj + assert: + that: + - odj_result is changed + - odj_result.odj_blob is defined + - odj_result.odj_blob == '' + - odj_result.odj_blob_path is defined + - odj_result.djoin is defined + - odj_result.djoin.invocation is defined + - odj_result.djoin.rc is defined + - odj_result.djoin.rc == 0 + - odj_result.djoin.stdout is defined + - odj_result.djoin.stderr is defined + + - name: This file needs to be deleted later + set_fact: + returned_file: "{{ odj_result.odj_blob_path }}" + + - name: Test ODJ File + ansible.windows.win_shell: | + $ErrorActionPreference = 'Stop' + $file = '{{ odj_result.odj_blob_path }}' + $content = Get-Content -LiteralPath $file -Raw -Encoding Unicode + $trimmed = $content.TrimEnd("`0") + if ($content.Length -eq $trimmed.Length) { throw 'No terminating null found' } + # try a base64 decode to validate it is the kind of data we expect + $bytes = [Convert]::FromBase64String($trimmed) + + - name: Create computer with offline domain join and random blob file return (idempotence) + win_domain_computer: + name: '{{ test_win_domain_computer_name }}' + dns_hostname: '{{ test_win_domain_computer_dns_hostname }}' + sam_account_name: '{{ test_win_domain_computer_sam_account_name }}' + ou: "{{ test_win_domain_computer_ou_path }}" + description: "{{ test_computer_description }}" + enabled: yes + state: present + offline_domain_join: path + register: odj_result + + - name: assert odj + assert: + that: + - odj_result is not changed + - odj_result.odj_blob is not defined + - odj_result.odj_blob_path is not defined + - odj_result.djoin is not defined + + always: + - name: ensure the test computer is deleted after the test + win_domain_computer: + name: '{{ test_win_domain_computer_name }}' + sam_account_name: '{{ test_win_domain_computer_sam_account_name }}' + state: absent + + - name: ensure the blob file is deleted + win_shell: | + Remove-Item -LiteralPath '{{ returned_file }}' -Force -ErrorAction SilentlyContinue + exit 0 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_domain_group/aliases b/ansible_collections/community/windows/tests/integration/targets/win_domain_group/aliases new file mode 100644 index 000000000..ad7ccf7ad --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_domain_group/aliases @@ -0,0 +1 @@ +unsupported diff --git a/ansible_collections/community/windows/tests/integration/targets/win_domain_group/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_domain_group/defaults/main.yml new file mode 100644 index 000000000..b02643ee0 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_domain_group/defaults/main.yml @@ -0,0 +1,3 @@ +test_win_domain_group_ldap_base: DC=ansible,DC=local +test_win_domain_group_ou_path: OU=ou1,DC=ansible,DC=local +test_win_domain_group_name: Moo Cow diff --git a/ansible_collections/community/windows/tests/integration/targets/win_domain_group/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_domain_group/tasks/main.yml new file mode 100644 index 000000000..1624928ea --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_domain_group/tasks/main.yml @@ -0,0 +1,353 @@ +# this won't run in Ansible's integration tests until we get a domain set up +# these are here if someone wants to run the module tests locally on their own +# domain. +# Requirements: +# LDAP Base path set in defaults/main.yml like DC=ansible,DC=local +# Custom OU path set in defaults/main.yml like OU=ou1,DC=ansible,DC=local +--- +- name: ensure the test group is deleted before the test + win_domain_group: + name: '{{test_win_domain_group_name}}' + state: absent + ignore_protection: True + +- name: fail pass in an invalid path + win_domain_group: + name: '{{test_win_domain_group_name}}' + state: present + organizational_unit: OU=fakeou,{{test_win_domain_group_ldap_base}} + register: fail_invalid_path + failed_when: fail_invalid_path.msg != 'the group path OU=fakeou,' + test_win_domain_group_ldap_base + ' does not exist, please specify a valid LDAP path' + +- name: create group with defaults check + win_domain_group: + name: '{{test_win_domain_group_name}}' + scope: global + state: present + register: create_default_check + check_mode: yes + +- name: get actual group with defaults check + ansible.windows.win_command: powershell.exe "Import-Module ActiveDirectory; Get-ADGroup -Identity '{{test_win_domain_group_name}}'" + register: create_default_actual_check + ignore_errors: True + +- name: assert create group with defaults checl + assert: + that: + - create_default_check is changed + - create_default_actual_check.rc == 1 + +- name: create group with defaults + win_domain_group: + name: '{{test_win_domain_group_name}}' + scope: global + state: present + register: create_default + +- name: get actual group with defaults + ansible.windows.win_command: powershell.exe "Import-Module ActiveDirectory; Get-ADGroup -Identity '{{test_win_domain_group_name}}'" + register: create_default_actual + +- name: assert create group with defaults + assert: + that: + - create_default is created + - create_default is changed + - create_default.category == 'Security' + - create_default.description == None + - create_default.display_name == None + - create_default.distinguished_name == 'CN=' + test_win_domain_group_name + ',CN=Users,' + test_win_domain_group_ldap_base + - create_default.group_scope == 'Global' + - create_default.guid is defined + - create_default.managed_by == None + - create_default.name == test_win_domain_group_name + - create_default.protected_from_accidental_deletion == False + - create_default.sid is defined + - create_default_actual.rc == 0 + +- name: create group with defaults again + win_domain_group: + name: '{{test_win_domain_group_name}}' + scope: global + state: present + register: create_default_again + +- name: assert create group with defaults again + assert: + that: + - create_default_again is not changed + - create_default_again is not created + +- name: remove group check + win_domain_group: + name: '{{test_win_domain_group_name}}' + state: absent + register: remove_group_check + check_mode: yes + +- name: get actual remove group check + ansible.windows.win_command: powershell.exe "Import-Module ActiveDirectory; Get-ADGroup -Identity '{{test_win_domain_group_name}}'" + register: remove_group_actual_check + +- name: assert remove group check + assert: + that: + - remove_group_check is changed + - remove_group_actual_check.rc == 0 + +- name: remove group + win_domain_group: + name: '{{test_win_domain_group_name}}' + state: absent + register: remove_group + +- name: get actual remove group + ansible.windows.win_command: powershell.exe "Import-Module ActiveDirectory; Get-ADGroup -Identity '{{test_win_domain_group_name}}'" + register: remove_group_actual + ignore_errors: True + +- name: assert remove group + assert: + that: + - remove_group is changed + - remove_group is not created + - remove_group_actual.rc == 1 + +- name: remove group again + win_domain_group: + name: '{{test_win_domain_group_name}}' + state: absent + register: remove_group_again + +- name: assert remove group again + assert: + that: + - remove_group_again is not changed + - remove_group_again is not created + +- name: create non default group check + win_domain_group: + name: '{{test_win_domain_group_name}}' + state: present + description: Group Description + display_name: Group Display Name + managed_by: Domain Admins + organizational_unit: '{{test_win_domain_group_ou_path}}' + category: distribution + scope: domainlocal + attributes: + mail: test@email.com + wWWHomePage: www.google.com + protect: True + register: create_non_default_check + check_mode: yes + +- name: get actual create non default group check + ansible.windows.win_command: powershell.exe "Import-Module ActiveDirectory; Get-ADGroup -Identity '{{test_win_domain_group_name}}'" + register: create_non_default_actual_check + ignore_errors: True + +- name: assert create non default group check + assert: + that: + - create_non_default_check is changed + - create_non_default_check is created + - create_non_default_actual_check.rc == 1 + +- name: create non default group + win_domain_group: + name: '{{test_win_domain_group_name}}' + state: present + description: Group Description + display_name: Group Display Name + managed_by: Domain Admins + organizational_unit: '{{test_win_domain_group_ou_path}}' + category: distribution + scope: domainlocal + attributes: + mail: test@email.com + wWWHomePage: www.google.com + protect: True + register: create_non_default + +- name: get actual create non default group + ansible.windows.win_command: powershell.exe "Import-Module ActiveDirectory; Get-ADGroup -Identity '{{test_win_domain_group_name}}'" + register: create_non_default_actual + ignore_errors: True + +- name: assert create non default group + assert: + that: + - create_non_default is changed + - create_non_default is created + - create_non_default.category == 'Distribution' + - create_non_default.description == 'Group Description' + - create_non_default.display_name == 'Group Display Name' + - create_non_default.distinguished_name == 'CN=' + test_win_domain_group_name + ',' + test_win_domain_group_ou_path + - create_non_default.group_scope == 'DomainLocal' + - create_non_default.guid is defined + - create_non_default.managed_by == 'CN=Domain Admins,CN=Users,' + test_win_domain_group_ldap_base + - create_non_default.name == test_win_domain_group_name + - create_non_default.protected_from_accidental_deletion == True + - create_non_default.sid is defined + - create_non_default.attributes.mail == 'test@email.com' + - create_non_default.attributes.wWWHomePage == 'www.google.com' + - create_non_default_actual.rc == 0 + +- name: create non default group again + win_domain_group: + name: '{{test_win_domain_group_name}}' + state: present + description: Group Description + display_name: Group Display Name + managed_by: Domain Admins + organizational_unit: '{{test_win_domain_group_ou_path}}' + category: distribution + scope: domainlocal + attributes: + mail: test@email.com + wWWHomePage: www.google.com + register: create_non_default_again + +- name: assert create non default group again + assert: + that: + - create_non_default_again is not changed + - create_non_default_again is not created + +- name: try and move group with protection mode on + win_domain_group: + name: '{{test_win_domain_group_name}}' + state: present + organizational_unit: CN=Users,{{test_win_domain_group_ldap_base}} + register: fail_move_with_protection + failed_when: fail_move_with_protection.msg != 'cannot move group ' + test_win_domain_group_name + ' when ProtectedFromAccidentalDeletion is turned on, run this module with ignore_protection=true to override this' + +- name: modify existing group check + win_domain_group: + name: '{{test_win_domain_group_name}}' + state: present + description: New Description + display_name: New Display Name + managed_by: Administrator + organizational_unit: 'CN=Users,{{test_win_domain_group_ldap_base}}' + category: security + scope: global + attributes: + mail: anothertest@email.com + ignore_protection: True + register: modify_existing_check + check_mode: yes + +- name: get actual of modify existing group check + ansible.windows.win_command: powershell.exe "Import-Module ActiveDirectory; (Get-ADGroup -Identity '{{test_win_domain_group_name}}').DistinguishedName" + register: modify_existing_actual_check + +- name: assert modify existing group check + assert: + that: + - modify_existing_check is changed + - modify_existing_check is not created + - modify_existing_actual_check.stdout == 'CN=' + test_win_domain_group_name + ',' + test_win_domain_group_ou_path + '\r\n' + +- name: modify existing group + win_domain_group: + name: '{{test_win_domain_group_name}}' + state: present + description: New Description + display_name: New Display Name + managed_by: Administrator + organizational_unit: CN=Users,{{test_win_domain_group_ldap_base}} + category: security + scope: global + attributes: + mail: anothertest@email.com + protect: True + ignore_protection: True + register: modify_existing + +- name: get actual of modify existing group + ansible.windows.win_command: powershell.exe "Import-Module ActiveDirectory; (Get-ADGroup -Identity '{{test_win_domain_group_name}}').DistinguishedName" + register: modify_existing_actual + +- name: assert modify existing group + assert: + that: + - modify_existing is changed + - modify_existing is not created + - modify_existing.category == 'Security' + - modify_existing.description == 'New Description' + - modify_existing.display_name == 'New Display Name' + - modify_existing.distinguished_name == 'CN=' + test_win_domain_group_name + ',CN=Users,' + test_win_domain_group_ldap_base + - modify_existing.group_scope == 'Global' + - modify_existing.guid is defined + - modify_existing.managed_by == 'CN=Administrator,CN=Users,' + test_win_domain_group_ldap_base + - modify_existing.name == test_win_domain_group_name + - modify_existing.protected_from_accidental_deletion == True + - modify_existing.sid is defined + - modify_existing.attributes.mail == 'anothertest@email.com' + - modify_existing_actual.stdout == 'CN=' + test_win_domain_group_name + ',CN=Users,' + test_win_domain_group_ldap_base + '\r\n' + +- name: modify existing group again + win_domain_group: + name: '{{test_win_domain_group_name}}' + state: present + description: New Description + display_name: New Display Name + managed_by: Administrator + organizational_unit: CN=Users,{{test_win_domain_group_ldap_base}} + category: Security + scope: global + attributes: + mail: anothertest@email.com + protect: True + ignore_protection: True + register: modify_existing_again + +- name: assert modify existing group again + assert: + that: + - modify_existing_again is not changed + - modify_existing_again is not created + +- name: fail change managed_by to invalid user + win_domain_group: + name: '{{test_win_domain_group_name}}' + state: present + scope: global + managed_by: fake user + register: fail_invalid_managed_by_user + failed_when: fail_invalid_managed_by_user.msg != 'failed to find managed_by user or group fake user to be used for comparison' + +- name: fail delete group with protection mode on + win_domain_group: + name: '{{test_win_domain_group_name}}' + state: absent + register: fail_delete_with_protection + failed_when: fail_delete_with_protection.msg != 'cannot delete group ' + test_win_domain_group_name + ' when ProtectedFromAccidentalDeletion is turned on, run this module with ignore_protection=true to override this' + +- name: delete group with protection mode on + win_domain_group: + name: '{{test_win_domain_group_name}}' + state: absent + ignore_protection: True + register: delete_with_force + +- name: get actual delete group with protection mode on + ansible.windows.win_command: powershell.exe "Import-Module ActiveDirectory; Get-ADGroup -Identity '{{test_win_domain_group_name}}'" + register: delete_with_force_actual + ignore_errors: True + +- name: assert delete group with protection mode on + assert: + that: + - delete_with_force is changed + - delete_with_force is not created + - delete_with_force_actual.rc == 1 + +- name: ensure the test group is deleted after the test + win_domain_group: + name: '{{test_win_domain_group_name}}' + state: absent + ignore_protection: True diff --git a/ansible_collections/community/windows/tests/integration/targets/win_domain_object_info/aliases b/ansible_collections/community/windows/tests/integration/targets/win_domain_object_info/aliases new file mode 100644 index 000000000..ad7ccf7ad --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_domain_object_info/aliases @@ -0,0 +1 @@ +unsupported diff --git a/ansible_collections/community/windows/tests/integration/targets/win_domain_object_info/handlers/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_domain_object_info/handlers/main.yml new file mode 100644 index 000000000..76a2a0f76 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_domain_object_info/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: remove test domain user + win_domain_user: + name: '{{ test_user.distinguished_name }}' + state: absent diff --git a/ansible_collections/community/windows/tests/integration/targets/win_domain_object_info/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_domain_object_info/tasks/main.yml new file mode 100644 index 000000000..89c977bfb --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_domain_object_info/tasks/main.yml @@ -0,0 +1,125 @@ +# These tests can't run in CI, this is really just a basic smoke tests for local runs. +--- +- name: assert better error message on auth failure + win_domain_object_info: + identity: id + register: fail_auth + failed_when: '"Failed to contact the AD server, this could be caused by the double hop problem" not in fail_auth.msg' + vars: + ansible_winrm_transport: ntlm + ansible_psrp_auth: ntlm + +- name: create test ad user + win_domain_user: + name: Ansible Test + firstname: Ansible + surname: Test + company: Contoso R Us + password: Password01 + state: present + password_never_expires: yes + groups: + - Domain Users + enabled: false + register: test_user + notify: remove test domain user + +- name: set a binary attribute and return other useful info missing from above + ansible.windows.win_shell: | + Set-ADUser -Identity '{{ test_user.sid }}' -Replace @{ audio = @([byte[]]@(1, 2, 3, 4), [byte[]]@(5, 6, 7, 8)) } + + $user = Get-ADUser -Identity '{{ test_user.sid }}' -Properties modifyTimestamp, ObjectGUID + + [TimeZoneInfo]::ConvertTimeToUtc($user.modifyTimestamp).ToString('o') + $user.ObjectGUID.ToString() + ([System.Security.Principal.SecurityIdentifier]'{{ test_user.sid }}').Translate([System.Security.Principal.NTAccount]).Value + register: test_user_extras + +- name: set other test info for easier access + set_fact: + test_user_mod_date: '{{ test_user_extras.stdout_lines[0] }}' + test_user_id: '{{ test_user_extras.stdout_lines[1] }}' + test_user_name: '{{ test_user_extras.stdout_lines[2] }}' + +- name: get properties for single user by DN + win_domain_object_info: + identity: '{{ test_user.distinguished_name }}' + register: by_identity + check_mode: yes # Just verifies it runs in check mode + +- name: assert get properties for single user by DN + assert: + that: + - not by_identity is changed + - by_identity.objects | length == 1 + - by_identity.objects[0].keys() | list | length == 4 + - by_identity.objects[0].DistinguishedName == test_user.distinguished_name + - by_identity.objects[0].Name == 'Ansible Test' + - by_identity.objects[0].ObjectClass == 'user' + - by_identity.objects[0].ObjectGUID == test_user_id + +- name: get specific properties by GUID + win_domain_object_info: + identity: '{{ test_user_id }}' + properties: + - audio # byte[] + - company # string + - department # not set + - logonCount # int + - modifyTimestamp # DateTime + - nTSecurityDescriptor # SecurityDescriptor as SDDL + - objectSID # SID + - ProtectedFromAccidentalDeletion # bool + - sAMAccountType # Test out the enum string attribute that we add + - userAccountControl # Test ou the enum string attribute that we add + register: by_guid_custom_props + +- name: assert get specific properties by GUID + assert: + that: + - not by_guid_custom_props is changed + - by_guid_custom_props.objects | length == 1 + - by_guid_custom_props.objects[0].DistinguishedName == test_user.distinguished_name + - by_guid_custom_props.objects[0].Name == 'Ansible Test' + - by_guid_custom_props.objects[0].ObjectClass == 'user' + - by_guid_custom_props.objects[0].ObjectGUID == test_user_id + - not by_guid_custom_props.objects[0].ProtectedFromAccidentalDeletion + - by_guid_custom_props.objects[0].audio == ['BQYHCA==', 'AQIDBA=='] + - by_guid_custom_props.objects[0].company == 'Contoso R Us' + - by_guid_custom_props.objects[0].department == None + - by_guid_custom_props.objects[0].logonCount == 0 + - by_guid_custom_props.objects[0].modifyTimestamp == test_user_mod_date + - by_guid_custom_props.objects[0].nTSecurityDescriptor.startswith('O:DAG:DAD:AI(') + - by_guid_custom_props.objects[0].objectSID.Name == test_user_name + - by_guid_custom_props.objects[0].objectSID.Sid == test_user.sid + - by_guid_custom_props.objects[0].sAMAccountType == 805306368 + - by_guid_custom_props.objects[0].sAMAccountType_AnsibleFlags == ['SAM_USER_OBJECT'] + - by_guid_custom_props.objects[0].userAccountControl == 66050 + - by_guid_custom_props.objects[0].userAccountControl_AnsibleFlags == ['ADS_UF_ACCOUNTDISABLE', 'ADS_UF_NORMAL_ACCOUNT', 'ADS_UF_DONT_EXPIRE_PASSWD'] + +- name: get invalid property + win_domain_object_info: + filter: sAMAccountName -eq 'Ansible Test' + properties: + - FakeProperty + register: invalid_prop_warning + +- name: assert get invalid property + assert: + that: + - not invalid_prop_warning is changed + - invalid_prop_warning.objects | length == 0 + - invalid_prop_warning.warnings | length == 1 + - '"Failed to retrieve properties for AD object" not in invalid_prop_warning.warnings[0]' + +- name: get by ldap filter returning multiple + win_domain_object_info: + ldap_filter: (&(objectClass=computer)(objectCategory=computer)) + properties: '*' + register: multiple_ldap + +- name: assert get by ldap filter returning multiple + assert: + that: + - not multiple_ldap is changed + - multiple_ldap.objects | length > 1 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_domain_ou/aliases b/ansible_collections/community/windows/tests/integration/targets/win_domain_ou/aliases new file mode 100644 index 000000000..22f581bfd --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_domain_ou/aliases @@ -0,0 +1,2 @@ +shippable/windows/group2 +skip/windows/2012 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_domain_ou/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_domain_ou/defaults/main.yml new file mode 100644 index 000000000..6892b03a6 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_domain_ou/defaults/main.yml @@ -0,0 +1,22 @@ +--- +win_domain_ou_test_type: default +win_domain_ou_root_path: DC=ansible,DC=test +win_domain_ou_structure: + - path: "{{ win_domain_ou_root_path }}" + name: VMware + - path: "OU=VMware,{{ win_domain_ou_root_path }}" + name: End User Computing + - path: "OU=End User Computing,OU=VMware,{{ win_domain_ou_root_path }}" + name: Workspace ONE Cloud Services + - path: "OU=Workspace ONE Cloud Services,OU=End User Computing,OU=VMware,{{ win_domain_ou_root_path }}" + name: SaaS Development and Enablement + +win_domain_ou_structure_check_mode: + - path: "{{ win_domain_ou_root_path }}" + name: VMware_check + - path: "OU=VMware_check,{{ win_domain_ou_root_path }}" + name: End User Computing + - path: "OU=End User Computing,OU=VMware_check,{{ win_domain_ou_root_path }}" + name: Workspace ONE Cloud Services + - path: "OU=Workspace ONE Cloud Services,OU=End User Computing,OU=VMware_check,{{ win_domain_ou_root_path }}" + name: SaaS Development and Enablement diff --git a/ansible_collections/community/windows/tests/integration/targets/win_domain_ou/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_domain_ou/meta/main.yml new file mode 100644 index 000000000..da6e52e2f --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_domain_ou/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - setup_domain_tests diff --git a/ansible_collections/community/windows/tests/integration/targets/win_domain_ou/tasks/check_mode_test.yml b/ansible_collections/community/windows/tests/integration/targets/win_domain_ou/tasks/check_mode_test.yml new file mode 100644 index 000000000..730298e40 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_domain_ou/tasks/check_mode_test.yml @@ -0,0 +1,116 @@ +--- +- name: Setup structure for checkmode + block: + - name: Ensure OU structure is present + community.windows.win_domain_ou: + name: "{{ item.name }}" + protected: false + path: "{{ item.path }}" + loop: "{{ win_domain_ou_structure }}" + register: test_setup_checkmode + failed_when: test_setup_checkmode is not changed + +- name: Run Check Mode Tests + block: + - name: Ensure OU is present (check_mode) + community.windows.win_domain_ou: + name: ansible_checkmode + register: test_check_mode_1 + failed_when: test_check_mode_1 is not changed + + - name: Ensure OU has updated properties (check_mode) + community.windows.win_domain_ou: + name: End User Computing + protected: true + path: "{{ win_domain_ou_root_path }}" + properties: + city: Sandy Springs + state: Georgia + streetaddress: 1155 Perimeter Center West + country: US + description: EUC Business Unit + postalcode: 30189 + register: test_check_mode_2 + failed_when: test_check_mode_2 is not changed + + - name: Ensure OU structure win_domain_ou_structure_check_mode is present (check_mode) + community.windows.win_domain_ou: + name: "{{ item.name }}" + protected: false + path: "{{ item.path }}" + loop: "{{ win_domain_ou_structure_check_mode }}" + register: test_check_mode_3 + failed_when: test_check_mode_3 is not changed + + - name: Ensure OU structure win_domain_ou_structure is present (check_mode) + community.windows.win_domain_ou: + name: "{{ item.name }}" + protected: false + path: "{{ item.path }}" + loop: "{{ win_domain_ou_structure }}" + register: test_check_mode_4 + failed_when: test_check_mode_4 is changed + + - name: Ensure OU structure is absent, recursive (check_mode) + community.windows.win_domain_ou: + name: VMware + path: "{{ win_domain_ou_root_path }}" + state: absent + recursive: true + register: test_check_mode_5 + failed_when: test_check_mode_5 is not changed + + - name: Ensure OU is present with specific properties (check_mode) + community.windows.win_domain_ou: + name: VMW Atlanta + path: "{{ win_domain_ou_root_path }}" + properties: + city: Sandy Springs + state: Georgia + streetaddress: 1155 Perimeter Center West + register: test_check_mode_6 + failed_when: test_check_mode_6 is not changed + + - name: Ensure OU is present with specific properties added (check_mode) + community.windows.win_domain_ou: + name: VMW Atlanta + path: "{{ win_domain_ou_root_path }}" + properties: + country: US + description: EUC Business Unit + postalcode: 30189 + register: test_check_mode_7 + failed_when: test_check_mode_7 is not changed + + - name: Ensure existing ou 'End User Computing' is absent (check_mode) + community.windows.win_domain_ou: + name: End User Computing + path: "OU=VMware,{{ win_domain_ou_root_path }}" + state: absent + register: test_check_mode_8 + failed_when: test_check_mode_8 is not changed + + - name: Ensure NonExisting OU 'VMW Atlanta' is absent (check_mode) + community.windows.win_domain_ou: + name: "VMW Atlanta" + path: "{{ win_domain_ou_root_path }}" + state: absent + register: test_check_mode_9 + failed_when: test_check_mode_9 is changed + check_mode: true + +- name: sanity check on check_mode + ansible.windows.win_shell: | + get-adorganizationalunit -Identity ansible_checkmode + register: test_sanity + failed_when: "'ansible_checkmode' in test_sanity.stdout" + changed_when: false + +- name: Teardown structure used for checkmode + community.windows.win_domain_ou: + name: VMware + path: "{{ win_domain_ou_root_path }}" + state: absent + recursive: true + register: test_teardown + failed_when: test_teardown is not changed diff --git a/ansible_collections/community/windows/tests/integration/targets/win_domain_ou/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_domain_ou/tasks/main.yml new file mode 100644 index 000000000..1ea02d5ee --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_domain_ou/tasks/main.yml @@ -0,0 +1,6 @@ +--- +- name: Run Tests + import_tasks: tests.yml + +- name: Run Check Mode Tests + import_tasks: check_mode_test.yml
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_domain_ou/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_domain_ou/tasks/tests.yml new file mode 100644 index 000000000..f5fe8d576 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_domain_ou/tasks/tests.yml @@ -0,0 +1,190 @@ +--- +- name: Ensure OU is present + community.windows.win_domain_ou: + name: AnsibleFest + register: test1 + failed_when: test1 is not changed + +- name: Ensure OU is present (idempotence check) + community.windows.win_domain_ou: + name: AnsibleFest + register: test1a + failed_when: test1a is changed + +- name: Ensure OU is absent + community.windows.win_domain_ou: + name: AnsibleFest + state: absent + register: test1_clean + failed_when: test1_clean is not changed + +- name: Ensure OU is absent (idempotence check) + community.windows.win_domain_ou: + name: AnsibleFest + state: absent + register: test1_clean_idempotent + failed_when: test1_clean_idempotent is changed + +- name: Ensure OU is present with path + community.windows.win_domain_ou: + name: End User Computing + path: "{{ win_domain_ou_root_path }}" + register: test2 + failed_when: test2 is not changed + +- name: Ensure OU is present with path (idempotence check) + community.windows.win_domain_ou: + name: End User Computing + path: "{{ win_domain_ou_root_path }}" + register: test2a + failed_when: test2a is changed + +- name: Ensure OU has updated properties + community.windows.win_domain_ou: + name: End User Computing + protected: true + path: "{{ win_domain_ou_root_path }}" + properties: + city: Sandy Springs + state: Georgia + streetaddress: 1155 Perimeter Center West + country: US + description: EUC Business Unit + postalcode: 30189 + register: test3 + failed_when: test3 is not changed + +- name: Ensure OU has updated properties (idempotence check) + community.windows.win_domain_ou: + name: End User Computing + protected: true + path: "{{ win_domain_ou_root_path }}" + properties: + city: Sandy Springs + state: Georgia + streetaddress: 1155 Perimeter Center West + country: US + description: EUC Business Unit + postalcode: 30189 + register: test3a + failed_when: test3a is changed + +- name: Ensure OU structure is present + community.windows.win_domain_ou: + name: "{{ item.name }}" + protected: false + path: "{{ item.path }}" + loop: "{{ win_domain_ou_structure }}" + register: test4 + failed_when: test4 is not changed + +- name: Ensure OU structure is present (idempotence check) + community.windows.win_domain_ou: + name: "{{ item.name }}" + protected: false + path: "{{ item.path }}" + loop: "{{ win_domain_ou_structure }}" + register: test4a + failed_when: test4a is changed + +- name: Ensure OU structure is absent, recursive + community.windows.win_domain_ou: + name: VMware + path: "{{ win_domain_ou_root_path }}" + state: absent + recursive: true + register: test5 + failed_when: test5 is not changed + +- name: Ensure OU structure is absent, recursive (idempotence check) + community.windows.win_domain_ou: + name: VMware + path: "{{ win_domain_ou_root_path }}" + state: absent + recursive: true + register: test5a + failed_when: test5a is changed + +- name: Ensure OU is present with specific properties + community.windows.win_domain_ou: + name: VMW Atlanta + path: "{{ win_domain_ou_root_path }}" + properties: + city: Sandy Springs + state: Georgia + streetaddress: 1155 Perimeter Center West + register: test6 + failed_when: test6 is not changed + +- name: Ensure OU is present with specific properties (idempotence check) + community.windows.win_domain_ou: + name: VMW Atlanta + path: "{{ win_domain_ou_root_path }}" + properties: + city: Sandy Springs + state: Georgia + streetaddress: 1155 Perimeter Center West + register: test6a + failed_when: test6a is changed + +- name: Ensure OU is present with specific properties added + community.windows.win_domain_ou: + name: VMW Atlanta + path: "{{ win_domain_ou_root_path }}" + properties: + country: US + description: EUC Business Unit + postalcode: 30189 + register: test7 + failed_when: test7 is not changed + +- name: Ensure OU is present with specific properties added (idempotence check) + community.windows.win_domain_ou: + name: VMW Atlanta + path: "{{ win_domain_ou_root_path }}" + properties: + country: US + description: EUC Business Unit + postalcode: 30189 + register: test7a + failed_when: test7a is changed + +- name: Ensure OU is absent + community.windows.win_domain_ou: + name: End User Computing + path: "{{ win_domain_ou_root_path }}" + state: absent + register: test8 + failed_when: test8 is not changed + +- name: Ensure OU is absent (idempotence check) + community.windows.win_domain_ou: + name: End User Computing + path: "{{ win_domain_ou_root_path }}" + state: absent + register: test8a + failed_when: test8a is changed + +- name: Ensure OU is absent + community.windows.win_domain_ou: + name: VMW Atlanta + path: "{{ win_domain_ou_root_path }}" + state: absent + register: test9 + failed_when: test9 is not changed + +- name: Ensure OU is absent (idempotence check) + community.windows.win_domain_ou: + name: VMW Atlanta + path: "{{ win_domain_ou_root_path }}" + state: absent + register: test9a + failed_when: test9a is changed + +- name: Assertions + assert: + that: + - test1a.ou.Name == "AnsibleFest" + - test3a.ou.StreetAddress == "1155 Perimeter Center West" + - test7a.ou.Country == "US" + - test6a.ou.City == "Sandy Springs" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_domain_user/aliases b/ansible_collections/community/windows/tests/integration/targets/win_domain_user/aliases new file mode 100644 index 000000000..22f581bfd --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_domain_user/aliases @@ -0,0 +1,2 @@ +shippable/windows/group2 +skip/windows/2012 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_domain_user/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_domain_user/meta/main.yml new file mode 100644 index 000000000..da6e52e2f --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_domain_user/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - setup_domain_tests diff --git a/ansible_collections/community/windows/tests/integration/targets/win_domain_user/tasks/check_mode_test.yml b/ansible_collections/community/windows/tests/integration/targets/win_domain_user/tasks/check_mode_test.yml new file mode 100644 index 000000000..755135421 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_domain_user/tasks/check_mode_test.yml @@ -0,0 +1,27 @@ +--- +- name: Create Justi (check_mode) + community.windows.win_domain_user: + name: Justi + password: J@n3P4ssw0rd# + state: present + update_password: on_create + account_locked: false + password_never_expires: false + enabled: true + register: new_user_check_mode + failed_when: + - not new_user_check_mode.changed + - not new_user_check_mode.created + check_mode: true + +- name: Sanity check on Check Mode + ansible.windows.win_powershell: + script: | + try { + Get-AdUser -Identity Justi + $Ansible.Failed = $true + } catch { + $Ansible.Failed = $false + } + register: sanity_check + changed_when: false diff --git a/ansible_collections/community/windows/tests/integration/targets/win_domain_user/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_domain_user/tasks/main.yml new file mode 100644 index 000000000..2edc6ce0e --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_domain_user/tasks/main.yml @@ -0,0 +1,18 @@ +--- +- name: Remove Users + win_domain_user: + name: "{{ item }}" + state: absent + loop: + - justi + - hana + - katie + +- name: Run Test Suite 1 + import_tasks: test1.yml + +- name: Run Test Suite 2 + import_tasks: test2.yml + +- name: Run Check Mode Tests + import_tasks: check_mode_test.yml diff --git a/ansible_collections/community/windows/tests/integration/targets/win_domain_user/tasks/test1.yml b/ansible_collections/community/windows/tests/integration/targets/win_domain_user/tasks/test1.yml new file mode 100644 index 000000000..a5ba7095c --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_domain_user/tasks/test1.yml @@ -0,0 +1,76 @@ +--- +- name: Justi | Create User + win_domain_user: + name: Justi + upn: justi@ansible.test + password: c0dinGwithKI@ + state: present + update_password: on_create + password_never_expires: false + enabled: true + spn: + - MSSQLSvc/US99DBSVR1 + - MSSQLSvc/US99DBSVR1.vmware.com + - MSSQLSvc/US99DBSVR1.vmware.com:1433 + register: new_user_test + failed_when: new_user_test is not success + +- name: Justi | Create User (idempotence check) + win_domain_user: + name: Justi + upn: justi@ansible.test + password: c0dinGwithKI@ + state: present + update_password: on_create + password_never_expires: false + enabled: true + spn: + - MSSQLSvc/US99DBSVR1 + - MSSQLSvc/US99DBSVR1.vmware.com + - MSSQLSvc/US99DBSVR1.vmware.com:1433 + register: new_user_test_idempotent + failed_when: new_user_test_idempotent is changed + +- name: Justi | Update Password + win_domain_user: + name: Justi + password: al3x@ndriastEch! + state: present + update_password: always + password_never_expires: false + enabled: true + register: password_changed + failed_when: not password_changed.changed + +- name: Justi | Replace SPNs + win_domain_user: + name: Justi + state: present + spn: + - MSSQLSvc/ + - MSSQLSvc/US99DBSVR1.vmware.com + register: spn_changed + failed_when: not spn_changed.changed + +- name: Justi | Add SPN + win_domain_user: + name: Justi + state: present + spn_action: add + spn: + - MSSQLSvc/US99DBSVR1.vmware.com:2433 + register: add_spn_changed + failed_when: add_spn_changed is not changed + +- name: Assertions + assert: + that: + - new_user_test.changed + - new_user_test.created + - not new_user_test.password_never_expires + - not new_user_test_idempotent.changed + - new_user_test_idempotent.distinguished_name == "CN=Justi,CN=Users,DC=ansible,DC=test" + - password_changed.changed + - password_changed.password_updated + - spn_changed.changed + - add_spn_changed.changed diff --git a/ansible_collections/community/windows/tests/integration/targets/win_domain_user/tasks/test2.yml b/ansible_collections/community/windows/tests/integration/targets/win_domain_user/tasks/test2.yml new file mode 100644 index 000000000..767ed538b --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_domain_user/tasks/test2.yml @@ -0,0 +1,171 @@ +--- +- name: Hana | Create User w/Invalid Password + win_domain_user: + name: hana + upn: hana@ansible.test + firstname: Hana + surname: Lytx + display_name: Hana Lytx + company: HelpMeExitVi Inc. + password: 123 + state: present + groups: + - Domain Admins + street: 123 TechTok St. + city: Sysengineer + state_province: OH + postal_code: 12345 + country: US + attributes: + telephoneNumber: 555-123456 + update_password: when_changed + password_never_expires: true + register: bad_password_test + failed_when: bad_password_test is success + +- name: Hana | Create User Again w/Valid Password + win_domain_user: + name: hana + upn: hana@ansible.test + firstname: Hana + surname: Lytx + display_name: Hana Lytx + company: HelpMeExitVi Inc. + password: h@nAlyTx18!X + state: present + groups: + - Domain Admins + street: 123 TechTok St. + city: Sysengineer + state_province: OH + postal_code: 12345 + country: US + attributes: + telephoneNumber: 555-123456 + update_password: when_changed + password_never_expires: true + register: good_password_test + failed_when: good_password_test is not success + +- name: Katie | Create User with Delegates + win_domain_user: + name: katie + firstname: Katie + surname: Kickscancer + display_name: Katie Kickscancer + password: SyNs@tI0N + update_password: on_create + state: present + delegates: + - CN=justi,CN=Users,DC=ansible,DC=test + spn: + - HTTPSvc/judge-svc1:80 + - HTTPSvc/gabrielle-svc1.vmware.com + register: delegates_test + failed_when: delegates_test is not success + +- name: Katie | Create User with Delegates (idempotence check) + win_domain_user: + name: katie + firstname: Katie + surname: Kickscancer + display_name: Katie Kickscancer + password: SyNs@tI0N + update_password: on_create + state: present + delegates: + - CN=justi,CN=Users,DC=ansible,DC=test + spn: + - HTTPSvc/judge-svc1:80 + - HTTPSvc/gabrielle-svc1.vmware.com + register: delegates_test_idempotent + failed_when: delegates_test_idempotent is changed + +- name: Katie | Remove SPN + win_domain_user: + name: katie + state: present + spn_action: remove + spn: + - HTTPSvc/gabrielle-svc1.vmware.com + register: remove_spn_test + failed_when: remove_spn_test is not changed + +- name: Katie | Remove SPN (idempotence check) + win_domain_user: + name: katie + state: present + spn_action: remove + spn: + - HTTPSvc/gabrielle-svc1.vmware.com + register: remove_spn_test_idempotent + failed_when: remove_spn_test_idempotent is changed + +- name: Katie | Add to groups that are missing - fail + win_domain_user: + name: katie + state: present + groups: + - Missing Group + register: add_invalid_group_fail + failed_when: add_invalid_group_fail is success + +- name: Katie | Add to groups that are missing - warn + win_domain_user: + name: katie + state: present + groups: + - Missing Group + groups_missing_behaviour: warn + register: add_invalid_group_warn + failed_when: not add_invalid_group_warn.warnings[0].startswith("Failed to locate group Missing Group but continuing on") + +- name: Katie | Add to groups that are missing - ignore + win_domain_user: + name: katie + state: present + groups: + - Missing Group + groups_missing_behaviour: ignore + register: add_invalid_group_ignore + failed_when: (add_invalid_group_ignore.warnings | default([]) | length) != 0 + +- name: Hana | Remove User + win_domain_user: + name: hana + state: absent + register: user_removed + failed_when: user_removed is not changed + +- name: Hana | Remove User (idempotence check) + win_domain_user: + name: hana + state: absent + register: user_removed_idempotent + failed_when: user_removed_idempotent is changed + +- name: Remove Justi + win_domain_user: + name: justi + state: absent + +- name: Remove Katie + win_domain_user: + name: katie + state: absent + +- name: Assertions + assert: + that: + - delegates_test is success + - not delegates_test_idempotent.changed + - not bad_password_test.changed + - good_password_test.changed + - good_password_test.upn == "hana@ansible.test" + - good_password_test.password_never_expires + - good_password_test.company == "HelpMeExitVi Inc." + - not good_password_test.created + - good_password_test.password_updated + - user_removed.state == "absent" + - not user_removed_idempotent.changed + - remove_spn_test.spn == ['HTTPSvc/judge-svc1:80'] diff --git a/ansible_collections/community/windows/tests/integration/targets/win_dotnet_ngen/aliases b/ansible_collections/community/windows/tests/integration/targets/win_dotnet_ngen/aliases new file mode 100644 index 000000000..3cf5b97e8 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_dotnet_ngen/aliases @@ -0,0 +1 @@ +shippable/windows/group3 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_dotnet_ngen/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_dotnet_ngen/tasks/main.yml new file mode 100644 index 000000000..146eeb3c5 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_dotnet_ngen/tasks/main.yml @@ -0,0 +1,20 @@ +# this only tests check mode as the full run can take several minutes to +# complete, this way we at least verify the script is parsable +--- +- name: run in check mode + win_dotnet_ngen: + register: result_check + check_mode: yes + +- name: assert run in check mode + assert: + that: + - result_check is changed + - result_check.dotnet_ngen_update_exit_code == 0 + - result_check.dotnet_ngen_update_output == "check mode output for C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319\\ngen.exe update /force" + - result_check.dotnet_ngen_eqi_exit_code == 0 + - result_check.dotnet_ngen_eqi_output == "check mode output for C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319\\ngen.exe executeQueuedItems" + - result_check.dotnet_ngen64_update_exit_code == 0 + - result_check.dotnet_ngen64_update_output == "check mode output for C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\ngen.exe update /force" + - result_check.dotnet_ngen64_eqi_exit_code == 0 + - result_check.dotnet_ngen64_eqi_output == "check mode output for C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\ngen.exe executeQueuedItems" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_eventlog/aliases b/ansible_collections/community/windows/tests/integration/targets/win_eventlog/aliases new file mode 100644 index 000000000..4f4664b68 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_eventlog/aliases @@ -0,0 +1 @@ +shippable/windows/group5 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_eventlog/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_eventlog/tasks/main.yml new file mode 100644 index 000000000..dcc075fcc --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_eventlog/tasks/main.yml @@ -0,0 +1,10 @@ +- name: Run tests for win_eventlog in normal mode + import_tasks: tests.yml + vars: + in_check_mode: no + +- name: Run tests for win_eventlog in check-mode + import_tasks: tests.yml + vars: + in_check_mode: yes + check_mode: yes diff --git a/ansible_collections/community/windows/tests/integration/targets/win_eventlog/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_eventlog/tasks/tests.yml new file mode 100644 index 000000000..94c231a4b --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_eventlog/tasks/tests.yml @@ -0,0 +1,447 @@ +# Test code for win_eventlog + +# (c) 2017, Andrew Saraceni <andrew.saraceni@gmail.com> +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +- name: Remove potentially leftover logs + win_eventlog: + name: "{{ item }}" + state: absent + with_items: + - WinEventLogTest + - NewWinEventLogTest + + +- name: Add log without sources + win_eventlog: + name: WinEventLogTest + state: present + register: add_log_without_sources + failed_when: add_log_without_sources.changed != false or add_log_without_sources.msg != "You must specify one or more sources when creating a log for the first time" + + +- name: Add log + win_eventlog: &wel_present + name: WinEventLogTest + sources: + - WinEventLogSource1 + - WinEventLogSource2 + state: present + register: add_log + +- name: Test add_log (normal mode) + assert: + that: + - add_log.changed == true + - add_log.exists == true + - add_log.sources == ["WinEventLogSource1", "WinEventLogSource2", "WinEventLogTest"] + - add_log.sources_changed == ["WinEventLogSource1", "WinEventLogSource2"] + when: not in_check_mode + +- name: Test add_log (check-mode) + assert: + that: + - add_log.changed == true + - add_log.exists == false + - add_log.sources_changed == [] + when: in_check_mode + + +- name: Add log (again) + win_eventlog: *wel_present + register: add_log_again + +- name: Test add_log_again (normal mode) + assert: + that: + - add_log_again.changed == false + - add_log_again.exists == true + - add_log_again.sources == ["WinEventLogSource1", "WinEventLogSource2", "WinEventLogTest"] + - add_log_again.sources_changed == [] + when: not in_check_mode + + +- name: Run tests for normal mode only (expects event log) + when: not in_check_mode + block: + + - name: Change default source + win_eventlog: + <<: *wel_present + sources: + - WinEventLogTest + category_file: C:\TestApp\AppCategories.dll + register: change_default_source + failed_when: change_default_source.changed != false or change_default_source.msg != "Cannot modify default source WinEventLogTest of log WinEventLogTest - you must remove the log" + + + - name: Change source category + win_eventlog: &welc_present + <<: *wel_present + sources: + - WinEventLogSource1 + category_file: C:\TestApp\AppCategories.dll + register: change_source_category + + - name: Test change_source_category + assert: + that: + - change_source_category.changed == true + - change_source_category.exists == true + - change_source_category.sources == ["WinEventLogSource1", "WinEventLogSource2", "WinEventLogTest"] + - change_source_category.sources_changed == ["WinEventLogSource1"] + + + - name: Change source category (again) + win_eventlog: *welc_present + register: change_source_category_again + + - name: Test change_source_category_again + assert: + that: + - change_source_category_again.changed == false + - change_source_category_again.exists == true + - change_source_category_again.sources == ["WinEventLogSource1", "WinEventLogSource2", "WinEventLogTest"] + - change_source_category_again.sources_changed == [] + + + - name: Change source message + win_eventlog: &welm_present + <<: *welc_present + message_file: C:\TestApp\AppMessages.dll + register: change_source_message + + - name: Test change_source_message + assert: + that: + - change_source_message.changed == true + - change_source_message.exists == true + - change_source_message.sources == ["WinEventLogSource1", "WinEventLogSource2", "WinEventLogTest"] + - change_source_message.sources_changed == ["WinEventLogSource1"] + + + - name: Change source message (again) + win_eventlog: *welm_present + register: change_source_message_again + + - name: Test change_source_message_again + assert: + that: + - change_source_message_again.changed == false + - change_source_message_again.exists == true + - change_source_message_again.sources == ["WinEventLogSource1", "WinEventLogSource2", "WinEventLogTest"] + - change_source_message_again.sources_changed == [] + + + - name: Change source parameter + win_eventlog: &welp_present + <<: *welm_present + parameter_file: C:\TestApp\AppParameters.dll + register: change_source_parameter + + - name: Test change_source_parameter + assert: + that: + - change_source_parameter.changed == true + - change_source_parameter.exists == true + - change_source_parameter.sources == ["WinEventLogSource1", "WinEventLogSource2", "WinEventLogTest"] + - change_source_parameter.sources_changed == ["WinEventLogSource1"] + + + - name: Change source parameter (again) + win_eventlog: *welp_present + register: change_source_parameter_again + + - name: Test change_source_parameter_again + assert: + that: + - change_source_parameter_again.changed == false + - change_source_parameter_again.exists == true + - change_source_parameter_again.sources == ["WinEventLogSource1", "WinEventLogSource2", "WinEventLogTest"] + - change_source_parameter_again.sources_changed == [] + + + - name: Change log maximum size + win_eventlog: &wels_present + <<: *wel_present + maximum_size: 256MB + register: change_log_maximum_size + + - name: Test change_log_maximum_size + assert: + that: + - change_log_maximum_size.changed == true + - change_log_maximum_size.exists == true + - change_log_maximum_size.maximum_size_kb == 262144 + + + - name: Change log maximum size (again) + win_eventlog: *wels_present + register: change_log_maximum_size_again + + - name: Test change_log_maximum_size_again + assert: + that: + - change_log_maximum_size_again.changed == false + - change_log_maximum_size_again.exists == true + - change_log_maximum_size_again.maximum_size_kb == 262144 + + + - name: Change log invalid maximum size 1 + win_eventlog: + <<: *wel_present + maximum_size: 256 MB + register: change_log_invalid_maximum_size_1 + failed_when: change_log_invalid_maximum_size_1.changed != false or change_log_invalid_maximum_size_1.msg != "Maximum size 256 MB is not properly specified" + + + - name: Change log invalid maximum size 2 + win_eventlog: + <<: *wel_present + maximum_size: 5GB + register: change_log_invalid_maximum_size_2 + failed_when: change_log_invalid_maximum_size_2.changed != false or change_log_invalid_maximum_size_2.msg != "Maximum size must be between 64KB and 4GB" + + + - name: Change log invalid maximum size 3 + win_eventlog: + <<: *wel_present + maximum_size: 129KB + register: change_log_invalid_maximum_size_3 + failed_when: change_log_invalid_maximum_size_3.changed != false or change_log_invalid_maximum_size_3.msg != "Maximum size must be divisible by 64KB" + + + - name: Change log retention days + win_eventlog: &welr_present + <<: *wels_present + retention_days: 128 + register: change_log_retention_days + + - name: Test change_log_retention_days + assert: + that: + - change_log_retention_days.changed == true + - change_log_retention_days.exists == true + - change_log_retention_days.retention_days == 128 + + + - name: Change log retention days (again) + win_eventlog: *welr_present + register: change_log_retention_days_again + + - name: Test change_log_retention_days_again + assert: + that: + - change_log_retention_days_again.changed == false + - change_log_retention_days_again.exists == true + - change_log_retention_days_again.retention_days == 128 + + + - name: Change log overflow action + win_eventlog: &welo_present + <<: *wels_present + overflow_action: OverwriteAsNeeded + register: change_log_overflow_action + + - name: Test change_log_overflow_action + assert: + that: + - change_log_overflow_action.changed == true + - change_log_overflow_action.exists == true + - change_log_overflow_action.overflow_action == "OverwriteAsNeeded" + + + - name: Change log overflow action (again) + win_eventlog: *welo_present + register: change_log_overflow_action_again + + - name: Test change_log_overflow_action_again + assert: + that: + - change_log_overflow_action_again.changed == false + - change_log_overflow_action_again.exists == true + - change_log_overflow_action_again.overflow_action == "OverwriteAsNeeded" + + + - name: Add log with existing source + win_eventlog: &wele_present + name: NewWinEventLogTest + sources: + - WinEventLogSource1 + state: present + register: add_log_with_existing_source + failed_when: add_log_with_existing_source.changed != false or add_log_with_existing_source.msg != "Source WinEventLogSource1 already exists and cannot be created" + + + - name: Add new log + win_eventlog: + <<: *wele_present + sources: + - NewWinEventLogSource1 + + - name: Change source for different log + win_eventlog: + <<: *wele_present + sources: + - WinEventLogSource1 + category_file: C:\TestApp\AppCategories.dll + register: change_source_for_different_log + failed_when: change_source_for_different_log.changed != false or change_source_for_different_log.msg != "Source WinEventLogSource1 does not belong to log NewWinEventLogTest and cannot be modified" + + - name: Remove new log + win_eventlog: + name: NewWinEventLogTest + state: absent + + + - name: Add entry to log + ansible.windows.win_shell: Write-EventLog -LogName WinEventLogTest -Source WinEventLogSource1 -EntryType Information -EventId 12345 -Message "Test message" + + - name: Verify add entry + win_eventlog: + name: WinEventLogTest + state: present + register: verify_add_entry + + - name: Test verify_add_entry + assert: + that: + - verify_add_entry.changed == false + - verify_add_entry.exists == true + - verify_add_entry.entries == 1 + + + - name: Clear log + win_eventlog: &wel_clear + name: WinEventLogTest + state: clear + register: clear_log + + - name: Test clear_log + assert: + that: + - clear_log.changed == true + - clear_log.exists == true + - clear_log.entries == 0 + when: not in_check_mode + + + - name: Clear log (again) + win_eventlog: *wel_clear + register: clear_log_again + + - name: Test clear_log_again + assert: + that: + - clear_log_again.changed == false + - clear_log_again.exists == true + - clear_log_again.entries == 0 + when: in_check_mode + + +- name: Clear absent log + win_eventlog: + name: WinEventLogTest + state: clear + register: clear_absent_log + when: in_check_mode + failed_when: clear_absent_log.changed != false or clear_absent_log.msg != "Cannot clear log WinEventLogTest as it does not exist" + + +- name: Remove default source + win_eventlog: &weld_absent + name: WinEventLogTest + sources: + - WinEventLogTest + state: absent + register: remove_default_source + failed_when: remove_default_source.changed != false or remove_default_source.msg != "Cannot remove default source WinEventLogTest from log WinEventLogTest - you must remove the log" + + +- name: Remove source + win_eventlog: &wels_absent + <<: *weld_absent + sources: + - WinEventLogSource1 + register: remove_source + +- name: Test remove_source (normal mode) + assert: + that: + - remove_source.changed == true + - remove_source.exists == true + - remove_source.sources == ["WinEventLogSource2", "WinEventLogTest"] + - remove_source.sources_changed == ["WinEventLogSource1"] + when: not in_check_mode + +- name: Test remove_source (check-mode) + assert: + that: + - remove_source.changed == false + - remove_source.exists == false + - remove_source.sources_changed == [] + when: in_check_mode + + +- name: Remove source (again) + win_eventlog: *wels_absent + register: remove_source_again + +- name: Test remove_source_again (normal mode) + assert: + that: + - remove_source_again.changed == false + - remove_source_again.exists == true + - remove_source.sources == ["WinEventLogSource2", "WinEventLogTest"] + - remove_source_again.sources_changed == [] + when: not in_check_mode + + +- name: Remove log + win_eventlog: &wel_absent + name: WinEventLogTest + state: absent + register: remove_log + +- name: Test remove_log (normal mode) + assert: + that: + - remove_log.changed == true + - remove_log.exists == false + - remove_log.sources_changed == ["WinEventLogSource2", "WinEventLogTest"] + when: not in_check_mode + +- name: Test remove_log (check-mode) + assert: + that: + - remove_log.changed == false + - remove_log.exists == false + - remove_log.sources_changed == [] + when: in_check_mode + + +- name: Remove log (again) + win_eventlog: *wel_absent + register: remove_log_again + +- name: Test remove_log_again (normal mode) + assert: + that: + - remove_log_again.changed == false + - remove_log_again.exists == false + - remove_log_again.sources_changed == [] + when: not in_check_mode diff --git a/ansible_collections/community/windows/tests/integration/targets/win_eventlog_entry/aliases b/ansible_collections/community/windows/tests/integration/targets/win_eventlog_entry/aliases new file mode 100644 index 000000000..215e0b069 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_eventlog_entry/aliases @@ -0,0 +1 @@ +shippable/windows/group4 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_eventlog_entry/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_eventlog_entry/defaults/main.yml new file mode 100644 index 000000000..611d16ec0 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_eventlog_entry/defaults/main.yml @@ -0,0 +1,6 @@ +win_test_log_source: + log: WinEventLogEntryTest + source: WinEventLogEntrySource +win_test_log_source_extra: + log: ExtraWinEventLogEntryTest + source: ExtraWinEventLogEntrySource diff --git a/ansible_collections/community/windows/tests/integration/targets/win_eventlog_entry/library/test_win_eventlog_entry.ps1 b/ansible_collections/community/windows/tests/integration/targets/win_eventlog_entry/library/test_win_eventlog_entry.ps1 new file mode 100644 index 000000000..2af179b5f --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_eventlog_entry/library/test_win_eventlog_entry.ps1 @@ -0,0 +1,33 @@ +#!powershell + +# (c) 2017, Andrew Saraceni <andrew.saraceni@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#Requires -Module Ansible.ModuleUtils.Legacy + +# Test module used to grab the latest entry from an event log and output its properties + +$ErrorActionPreference = "Stop" + +$params = Parse-Args $args -supports_check_mode $true +$log = Get-AnsibleParam -obj $params -name "log" -type "str" -failifempty $true + +$result = @{ + changed = $false +} + +try { + $log_entry = Get-EventLog -LogName $log | Select-Object -First 1 -Property * +} +catch { + Fail-Json -obj $result -message "Could not find any entries for log $log" +} + +$result.source = $log_entry.Source +$result.event_id = $log_entry.EventID +$result.message = $log_entry.Message +$result.entry_type = $log_entry.EntryType.ToString() +$result.category = $log_entry.CategoryNumber +$result.raw_data = $log_entry.Data -join "," + +Exit-Json -obj $result diff --git a/ansible_collections/community/windows/tests/integration/targets/win_eventlog_entry/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_eventlog_entry/tasks/main.yml new file mode 100644 index 000000000..9f416598e --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_eventlog_entry/tasks/main.yml @@ -0,0 +1,33 @@ +# win_shell invocations can eventually be replaced with win_eventlog +- name: Remove potentially leftover test logs and sources + ansible.windows.win_shell: Remove-EventLog -LogName "{{ item.log }}" -ErrorAction SilentlyContinue + with_items: + - "{{ win_test_log_source }}" + - "{{ win_test_log_source_extra }}" + failed_when: no + +- name: Add new test logs and sources + ansible.windows.win_shell: New-EventLog -LogName "{{ item.log }}" -Source "{{ item.source }}" + with_items: + - "{{ win_test_log_source }}" + - "{{ win_test_log_source_extra }}" + +- name: Run tests for win_eventlog_entry + block: + + - name: Test in normal mode + import_tasks: tests.yml + vars: + in_check_mode: no + + - name: Test in check-mode + import_tasks: tests.yml + vars: + in_check_mode: yes + check_mode: yes + +- name: Remove test logs and sources + ansible.windows.win_shell: Remove-EventLog -LogName "{{ item.log }}" + with_items: + - "{{ win_test_log_source }}" + - "{{ win_test_log_source_extra }}" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_eventlog_entry/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_eventlog_entry/tasks/tests.yml new file mode 100644 index 000000000..688a4b532 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_eventlog_entry/tasks/tests.yml @@ -0,0 +1,159 @@ +# Test code for win_eventlog_entry + +# (c) 2017, Andrew Saraceni <andrew.saraceni@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +- name: Add entry to fake log + win_eventlog_entry: + log: FakeLogName + source: "{{ win_test_log_source.source }}" + event_id: 12345 + message: This is a test log entry message + register: add_entry_to_fake_log + failed_when: add_entry_to_fake_log.changed != false or add_entry_to_fake_log.msg != "Log FakeLogName does not exist and cannot be written to" + + +- name: Add entry from fake source + win_eventlog_entry: + log: "{{ win_test_log_source.log }}" + source: FakeSourceName + event_id: 12345 + message: This is a test log entry message + register: add_entry_from_fake_source + failed_when: add_entry_from_fake_source.changed != false or add_entry_from_fake_source.msg != "Source FakeSourceName does not exist" + + +- name: Add entry with invalid event_id + win_eventlog_entry: + log: "{{ win_test_log_source.log }}" + source: "{{ win_test_log_source.source }}" + event_id: 67000 + message: This is a test log entry message + register: add_entry_with_invalid_event_id + failed_when: add_entry_with_invalid_event_id.changed != false or add_entry_with_invalid_event_id.msg != "Event ID must be between 0 and 65535" + + +- name: Add entry from other log source + win_eventlog_entry: + log: "{{ win_test_log_source.log }}" + source: "{{ win_test_log_source_extra.source }}" + event_id: 12345 + message: This is a test log entry message + register: add_entry_from_other_log_source + failed_when: add_entry_from_other_log_source.changed != false or add_entry_from_other_log_source.msg != "Source {{ win_test_log_source_extra.source }} does not belong to log {{ win_test_log_source.log }} and cannot be written to" + + +- name: Add entry + win_eventlog_entry: &wele + log: "{{ win_test_log_source.log }}" + source: "{{ win_test_log_source.source }}" + event_id: 12345 + message: This is a test log entry message + register: add_entry + +- name: Test add_entry + assert: + that: + - add_entry.changed == true + - add_entry.msg == "Entry added to log {{ win_test_log_source.log }} from source {{ win_test_log_source.source }}" + +- name: Test add_entry count (normal mode) + ansible.windows.win_shell: (Get-EventLog -LogName "{{ win_test_log_source.log }}").Count + register: add_entry_count + failed_when: add_entry_count.stdout_lines[0] != "1" + when: not in_check_mode + +- name: Test add_entry result (normal mode) + test_win_eventlog_entry: + log: "{{ win_test_log_source.log }}" + register: add_entry_result + when: not in_check_mode + +- name: Test add_entry_result (normal mode) + assert: + that: + - add_entry_result.source == win_test_log_source.source + - add_entry_result.event_id == 12345 + - add_entry_result.message == "This is a test log entry message" + when: not in_check_mode + + +- name: Add entry (again) + win_eventlog_entry: *wele + register: add_entry_again + +- name: Test add_entry_again (normal mode) + assert: + that: + - add_entry_again.changed == true + - add_entry_again.msg == "Entry added to log {{ win_test_log_source.log }} from source {{ win_test_log_source.source }}" + when: not in_check_mode + +- name: Test add_entry_again count (normal mode) + ansible.windows.win_shell: (Get-EventLog -LogName "{{ win_test_log_source.log }}").Count + register: add_entry_again_count + failed_when: add_entry_again_count.stdout_lines[0] != "2" + when: not in_check_mode + + +- name: Add entry all options + win_eventlog_entry: &wele_ao + <<: *wele + event_id: 500 + message: This is a test error message + entry_type: Error + category: 5 + raw_data: 10,20 + register: add_entry_all_options + +- name: Test add_entry_all_options + assert: + that: + - add_entry_all_options.changed == true + - add_entry_all_options.msg == "Entry added to log {{ win_test_log_source.log }} from source {{ win_test_log_source.source }}" + +- name: Test add_entry_all_options count (normal mode) + ansible.windows.win_shell: (Get-EventLog -LogName "{{ win_test_log_source.log }}").Count + register: add_entry_all_options_count + failed_when: add_entry_all_options_count.stdout_lines[0] != "3" + when: not in_check_mode + +- name: Test add_entry_all_options result (normal mode) + test_win_eventlog_entry: + log: "{{ win_test_log_source.log }}" + register: add_entry_all_options_result + when: not in_check_mode + +- name: Test add_entry_all_options_result (normal mode) + assert: + that: + - add_entry_all_options_result.source == win_test_log_source.source + - add_entry_all_options_result.event_id == 500 + - add_entry_all_options_result.message == "This is a test error message" + - add_entry_all_options_result.entry_type == "Error" + - add_entry_all_options_result.category == 5 + - add_entry_all_options_result.raw_data == "10,20" + when: not in_check_mode + + +- name: Add entry all options (again) + win_eventlog_entry: *wele_ao + register: add_entry_all_options_again + +- name: Test add_entry_all_options_again (normal mode) + assert: + that: + - add_entry_all_options_again.changed == true + - add_entry_all_options_again.msg == "Entry added to log {{ win_test_log_source.log }} from source {{ win_test_log_source.source }}" + when: not in_check_mode + +- name: Test add_entry_all_options_again count (normal mode) + ansible.windows.win_shell: (Get-EventLog -LogName "{{ win_test_log_source.log }}").Count + register: add_entry_all_options_again_count + failed_when: add_entry_all_options_again_count.stdout_lines[0] != "4" + when: not in_check_mode + + +- name: Clear event log entries + ansible.windows.win_shell: Clear-EventLog -LogName "{{ win_test_log_source.log }}" + when: not in_check_mode diff --git a/ansible_collections/community/windows/tests/integration/targets/win_feature_info/aliases b/ansible_collections/community/windows/tests/integration/targets/win_feature_info/aliases new file mode 100644 index 000000000..215e0b069 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_feature_info/aliases @@ -0,0 +1 @@ +shippable/windows/group4 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_feature_info/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_feature_info/defaults/main.yml new file mode 100644 index 000000000..01cc5ee55 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_feature_info/defaults/main.yml @@ -0,0 +1,2 @@ +--- +test_feature: Telnet-Client diff --git a/ansible_collections/community/windows/tests/integration/targets/win_feature_info/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_feature_info/tasks/main.yml new file mode 100644 index 000000000..a6fdb28f6 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_feature_info/tasks/main.yml @@ -0,0 +1,58 @@ +--- +- name: test we can get info for all features + win_feature_info: + register: all_actual + check_mode: yes # tests that this will run in check mode + +- name: assert test we can get info for all features + assert: + that: + - all_actual.exists + - not all_actual is changed + +- name: test info on a missing feature + win_feature_info: + name: ansible_feature_info_missing + register: missing_feature + +- name: assert test info on a missing feature + assert: + that: + - not missing_feature is changed + - not missing_feature.exists + +- name: Install Test Feature + ansible.windows.win_feature: + name: "{{ test_feature }}" + state: present + +- name: test info on a single Feature + win_feature_info: + name: '{{ test_feature }}' + register: specific_feature_present + +- name: assert test info on single feature + assert: + that: + - not specific_feature_present is changed + - specific_feature_present.exists + - specific_feature_present.features | length == 1 + - specific_feature_present.features[0].install_state == "Installed" + +- name: Uninstall Test Feature + ansible.windows.win_feature: + name: "{{ test_feature }}" + state: absent + +- name: test info on a single Feature + win_feature_info: + name: '{{ test_feature }}' + register: specific_feature_absent + +- name: assert test info on single feature + assert: + that: + - not specific_feature_absent is changed + - specific_feature_absent.exists + - specific_feature_absent.features | length == 1 + - specific_feature_absent.features[0].install_state == "Available" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_file_compression/aliases b/ansible_collections/community/windows/tests/integration/targets/win_file_compression/aliases new file mode 100644 index 000000000..215e0b069 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_file_compression/aliases @@ -0,0 +1 @@ +shippable/windows/group4 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_file_compression/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_file_compression/defaults/main.yml new file mode 100644 index 000000000..ae24afe7c --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_file_compression/defaults/main.yml @@ -0,0 +1,5 @@ +test_win_file_compression_suffix: win_file_compression .ÅÑŚÌβŁÈ [$!@^&test(;)] +test_win_file_compression_sub_directories: + - 'a' + - 'b' +test_win_file_compression_filename: 'foo.bar' diff --git a/ansible_collections/community/windows/tests/integration/targets/win_file_compression/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_file_compression/meta/main.yml new file mode 100644 index 000000000..9f37e96cd --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_file_compression/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_remote_tmp_dir diff --git a/ansible_collections/community/windows/tests/integration/targets/win_file_compression/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_file_compression/tasks/main.yml new file mode 100644 index 000000000..542728bd7 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_file_compression/tasks/main.yml @@ -0,0 +1,224 @@ +--- +- name: set fact of special testing dir + set_fact: + test_directory: '{{ remote_tmp_dir }}\{{ test_win_file_compression_suffix }}' + +- name: create sub directories + ansible.windows.win_file: + state: directory + path: "{{ test_directory }}\\{{ item }}" + loop: "{{ test_win_file_compression_sub_directories }}" + +- name: set main directory as hidden to test out edge cases + ansible.windows.win_shell: (Get-Item -LiteralPath '{{ test_directory }}').Attributes = [System.IO.FileAttributes]::Hidden + +- name: Compress parent directory + win_file_compression: + path: "{{ test_directory }}" + state: present + register: result + +- name: Get actual attributes for parent directory + ansible.windows.win_stat: + path: "{{ test_directory }}" + register: folder_info + +- assert: + that: + - "'Compressed' in folder_info.stat.attributes" + - "result.changed == true" + +- name: Get actual attributes for sub directories + ansible.windows.win_stat: + path: "{{ test_directory }}\\{{ item }}" + register: subfolder_info + loop: "{{ test_win_file_compression_sub_directories }}" + +- assert: + that: + - "'Compressed' not in item.stat.attributes" + loop: "{{ subfolder_info.results }}" + +- name: Compress parent directory (idempotent) + win_file_compression: + path: "{{ test_directory }}" + state: present + register: result + +- assert: + that: + - "result.changed == false" + +- name: Compress parent directory and all subdirectories + win_file_compression: + path: "{{ test_directory }}" + state: present + recurse: yes + register: result + +- name: Get actual attributes for parent directory + ansible.windows.win_stat: + path: "{{ test_directory }}" + register: folder_info + +- assert: + that: + - "'Compressed' in folder_info.stat.attributes" + - "result.changed == true" + +- name: Get actual attributes for sub directories + ansible.windows.win_stat: + path: "{{ test_directory }}\\{{ item }}" + register: subfolder_info + loop: "{{ test_win_file_compression_sub_directories }}" + +- assert: + that: + - "'Compressed' in item.stat.attributes" + loop: "{{ subfolder_info.results }}" + +- name: Compress parent directory and all subdirectories (idempotent) + win_file_compression: + path: "{{ test_directory }}" + state: present + recurse: yes + register: result + +- assert: + that: + - "result.changed == false" + +- name: Uncompress parent directory + win_file_compression: + path: "{{ test_directory }}" + state: absent + recurse: no + register: result + +- name: Get actual attributes for parent directory + ansible.windows.win_stat: + path: "{{ test_directory }}" + register: folder_info + +- assert: + that: + - "'Compressed' not in folder_info.stat.attributes" + - "result.changed == true" + +- name: Get actual attributes for sub directories + ansible.windows.win_stat: + path: "{{ test_directory }}\\{{ item }}" + register: subfolder_info + loop: "{{ test_win_file_compression_sub_directories }}" + +- assert: + that: + - "'Compressed' in item.stat.attributes" + loop: "{{ subfolder_info.results }}" + +- name: Uncompress parent directory (idempotent) + win_file_compression: + path: "{{ test_directory }}" + state: absent + recurse: no + register: result + +- assert: + that: + - "result.changed == false" + +- name: Uncompress parent directory and all subdirectories + win_file_compression: + path: "{{ test_directory }}" + state: absent + recurse: yes + register: result + +- name: Get actual attributes for parent directory + ansible.windows.win_stat: + path: "{{ test_directory }}" + register: folder_info + +- assert: + that: + - "'Compressed' not in folder_info.stat.attributes" + - "result.changed == true" + +- name: Get actual attributes for sub directories + ansible.windows.win_stat: + path: "{{ test_directory }}\\{{ item }}" + register: subfolder_info + loop: "{{ test_win_file_compression_sub_directories }}" + +- assert: + that: + - "'Compressed' not in item.stat.attributes" + loop: "{{ subfolder_info.results }}" + +- name: Uncompress parent directory and all subdirectories (idempotent) + win_file_compression: + path: "{{ test_directory }}" + state: absent + recurse: yes + register: result + +- assert: + that: + - "result.changed == false" + +- name: Create test file + ansible.windows.win_file: + state: touch + path: "{{ test_directory }}\\{{ test_win_file_compression_filename }}" + +- name: Compress specific file + win_file_compression: + path: "{{ test_directory }}\\{{ test_win_file_compression_filename }}" + state: present + register: result + +- name: Get actual attributes of file + ansible.windows.win_stat: + path: "{{ test_directory }}\\{{ test_win_file_compression_filename }}" + register: testfile_info + +- assert: + that: + - "result.changed == true" + - "'Compressed' in testfile_info.stat.attributes" + +- name: Compress specific file (idempotent) + win_file_compression: + path: "{{ test_directory }}\\{{ test_win_file_compression_filename }}" + state: present + register: result + +- assert: + that: + - "result.changed == false" + +- name: Uncompress specific file + win_file_compression: + path: "{{ test_directory }}\\{{ test_win_file_compression_filename }}" + state: absent + register: result + +- name: Get actual attributes of file + ansible.windows.win_stat: + path: "{{ test_directory }}\\{{ test_win_file_compression_filename }}" + register: testfile_info + +- assert: + that: + - "result.changed == true" + - "'Compressed' not in testfile_info.stat.attributes" + +- name: Uncompress specific file (idempotent) + win_file_compression: + path: "{{ test_directory }}\\{{ test_win_file_compression_filename }}" + state: absent + register: result + +- assert: + that: + - "result.changed == false" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_firewall/aliases b/ansible_collections/community/windows/tests/integration/targets/win_firewall/aliases new file mode 100644 index 000000000..c8fd90a1f --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_firewall/aliases @@ -0,0 +1,3 @@ +shippable/windows/group5 +skip/windows/2012 +skip/windows/2012-R2 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_firewall/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_firewall/tasks/main.yml new file mode 100644 index 000000000..d1e4d89c4 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_firewall/tasks/main.yml @@ -0,0 +1,52 @@ +# NOTE: The win_firewall module only works on WMF 5+ + +- ansible.windows.setup: + +- name: Test Windows capabilities + raw: Get-Command Get-NetFirewallProfile -ErrorAction SilentlyContinue; return $? + failed_when: no + register: get_netfirewallprofile + +- name: Only run tests when Windows is capable + when: get_netfirewallprofile.rc == 0 and ansible_powershell_version >= 5 + block: + - name: Turn off Windows Firewall (begin) + win_firewall: + profiles: [ Domain, Private, Public ] + state: disabled + register: firewall_off + + - name: Test firewall_off + assert: + that: + - not firewall_off.Domain.enabled + - not firewall_off.Private.enabled + - not firewall_off.Public.enabled + + + - name: Test in normal mode + import_tasks: tests.yml + vars: + in_check_mode: no + + + - name: Test in check-mode + import_tasks: tests.yml + vars: + in_check_mode: yes + check_mode: yes + + + - name: Turn on Windows Firewall (end) + win_firewall: + profiles: [ Domain, Private, Public ] + state: enabled + register: firewall_on + + - name: Test firewall_on + assert: + that: + - firewall_on is changed + - firewall_on.Domain.enabled + - firewall_on.Private.enabled + - firewall_on.Public.enabled diff --git a/ansible_collections/community/windows/tests/integration/targets/win_firewall/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_firewall/tasks/tests.yml new file mode 100644 index 000000000..80b5f1553 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_firewall/tasks/tests.yml @@ -0,0 +1,268 @@ +# We start with firewall turned off + +- name: Turn off Windows Firewall again + win_firewall: + profiles: [ Domain, Private, Public ] + state: disabled + register: firewall_off_again + +- name: Test firewall_off_again + assert: + that: + - firewall_off_again is not changed + - not firewall_off_again.Domain.enabled + - not firewall_off_again.Private.enabled + - not firewall_off_again.Public.enabled + +- name: Turn on Windows Firewall on Public + win_firewall: + profiles: [ Public ] + state: enabled + register: firewall_public_on + +- name: Test firewall_public_on + assert: + that: + - firewall_public_on is changed + - not firewall_public_on.Domain.enabled + - not firewall_public_on.Private.enabled + - firewall_public_on.Public.enabled + + +- name: Turn on Windows Firewall on Public again + win_firewall: + profiles: [ Public ] + state: enabled + register: firewall_public_on_again + +- name: Test firewall_public_on_again (normal mode) + assert: + that: + - firewall_public_on_again is not changed + - not firewall_public_on_again.Domain.enabled + - not firewall_public_on_again.Private.enabled + - firewall_public_on_again.Public.enabled + when: not in_check_mode + +- name: Test firewall_public_on_again (check-mode) + assert: + that: + - firewall_public_on_again is changed + - not firewall_public_on_again.Domain.enabled + - not firewall_public_on_again.Private.enabled + - firewall_public_on_again.Public.enabled + when: in_check_mode + + +# On purpose not a list +- name: Turn on Windows Firewall on Domain + win_firewall: + profiles: Domain + state: enabled + register: firewall_domain_on + +- name: Test firewall_domain_on (normal mode) + assert: + that: + - firewall_domain_on is changed + - firewall_domain_on.Domain.enabled + - not firewall_domain_on.Private.enabled + - firewall_domain_on.Public.enabled + when: not in_check_mode + +- name: Test firewall_domain_on (check-mode) + assert: + that: + - firewall_domain_on is changed + - firewall_domain_on.Domain.enabled + - not firewall_domain_on.Private.enabled + - not firewall_domain_on.Public.enabled + when: in_check_mode + + +- name: Turn on Windows Firewall on Domain again + win_firewall: + profiles: [ Domain ] + state: enabled + register: firewall_domain_on_again + +- name: Test firewall_domain_on_again (normal mode) + assert: + that: + - firewall_domain_on_again is not changed + - firewall_domain_on.Domain.enabled + - not firewall_domain_on.Private.enabled + - firewall_domain_on.Public.enabled + when: not in_check_mode + +- name: Test firewall_domain_on_again (check-mode) + assert: + that: + - firewall_domain_on_again is changed + - firewall_domain_on.Domain.enabled + - not firewall_domain_on.Private.enabled + - not firewall_domain_on.Public.enabled + when: in_check_mode + + +- name: Turn on Windows Firewall + win_firewall: + profiles: [ Domain, Private, Public ] + state: enabled + register: firewall_on + +- name: Test firewall_on + assert: + that: + - firewall_on is changed + - firewall_on.Domain.enabled + - firewall_on.Private.enabled + - firewall_on.Public.enabled + +- name: Turn on Windows Firewall on Domain with allow inbound connection + win_firewall: + profiles: Domain + state: enabled + inbound_action: allow + register: firewall_domain_on + +- name: Test firewall_domain_on (normal mode) + assert: + that: + - firewall_domain_on is changed + - firewall_domain_on.Domain.enabled + when: not in_check_mode + +- name: Test firewall_domain_on (check-mode) + assert: + that: + - firewall_domain_on is changed + - firewall_domain_on.Domain.enabled + when: in_check_mode + +- name: Turn on Windows Firewall on Domain again with allow inbound + win_firewall: + profiles: [ Domain ] + state: enabled + inbound_action: allow + register: firewall_domain_on_again + +- name: Test firewall_domain_on_again (normal mode) + assert: + that: + - firewall_domain_on_again is not changed + - firewall_domain_on.Domain.enabled + when: not in_check_mode + +- name: Test firewall_domain_on_again (check-mode) + assert: + that: + - firewall_domain_on_again is changed + - firewall_domain_on.Domain.enabled + when: in_check_mode + +- name: Turn on Windows Firewall on Domain with block outbound connection + win_firewall: + profiles: Domain + state: enabled + outbound_action: block + register: firewall_domain_on + +- name: Test firewall_domain_on (normal mode) + assert: + that: + - firewall_domain_on is changed + - firewall_domain_on.Domain.enabled + when: not in_check_mode + +- name: Test firewall_domain_on (check-mode) + assert: + that: + - firewall_domain_on is changed + - firewall_domain_on.Domain.enabled + when: in_check_mode + +- name: Turn on Windows Firewall on Domain again with block outbound connection + win_firewall: + profiles: [ Domain ] + state: enabled + outbound_action: block + register: firewall_domain_on_again + +- name: Test firewall_domain_on_again (normal mode) + assert: + that: + - firewall_domain_on_again is not changed + - firewall_domain_on.Domain.enabled + when: not in_check_mode + +- name: Test firewall_domain_on_again (check-mode) + assert: + that: + - firewall_domain_on_again is changed + - firewall_domain_on.Domain.enabled + when: in_check_mode + +# On purpose no profiles added +- name: Turn on Windows Firewall again + win_firewall: + state: enabled + register: firewall_on_again + +- name: Test firewall_on_again (normal mode) + assert: + that: + - firewall_on_again is not changed + - firewall_on_again.Domain.enabled + - firewall_on_again.Private.enabled + - firewall_on_again.Public.enabled + when: not in_check_mode + +- name: Test firewall_on_again (check-mode) + assert: + that: + - firewall_on_again is changed + - firewall_on_again.Domain.enabled + - firewall_on_again.Private.enabled + - firewall_on_again.Public.enabled + when: in_check_mode + + +# On purpose no profiles added +- name: Turn off Windows Firewall + win_firewall: + state: disabled + register: firewall_off2 + +- name: Test firewall_off2 (normal mode) + assert: + that: + - firewall_off2 is changed + - not firewall_off2.Domain.enabled + - not firewall_off2.Private.enabled + - not firewall_off2.Public.enabled + when: not in_check_mode + +- name: Test firewall_off2 (check-mode) + assert: + that: + - firewall_off2 is not changed + - not firewall_off2.Domain.enabled + - not firewall_off2.Private.enabled + - not firewall_off2.Public.enabled + when: in_check_mode + + +- name: Turn off Windows Firewall again + win_firewall: + profiles: [ Domain, Private, Public ] + state: disabled + register: firewall_off2_again + +- name: Test firewall_off2_again (normal mode) + assert: + that: + - firewall_off2_again is not changed + - not firewall_off2_again.Domain.enabled + - not firewall_off2_again.Private.enabled + - not firewall_off2_again.Public.enabled diff --git a/ansible_collections/community/windows/tests/integration/targets/win_firewall_rule/aliases b/ansible_collections/community/windows/tests/integration/targets/win_firewall_rule/aliases new file mode 100644 index 000000000..4cd27b3cb --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_firewall_rule/aliases @@ -0,0 +1 @@ +shippable/windows/group1 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_firewall_rule/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_firewall_rule/tasks/main.yml new file mode 100644 index 000000000..21fe38196 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_firewall_rule/tasks/main.yml @@ -0,0 +1,609 @@ +- name: Remove potentially leftover firewall rule + win_firewall_rule: + name: http + state: absent + action: allow + direction: in + +- name: Add firewall rule + win_firewall_rule: + name: http + enabled: yes + state: present + localport: 80 + action: allow + direction: in + protocol: tcp + register: add_firewall_rule + +- name: Check that creating new firewall rule succeeds with a change + assert: + that: + - add_firewall_rule.changed == true + +- name: Add same firewall rule (again) + win_firewall_rule: + name: http + enabled: yes + state: present + localport: 80 + action: allow + direction: in + protocol: tcp + register: add_firewall_rule_again + +- name: Check that creating same firewall rule succeeds without a change + assert: + that: + - add_firewall_rule_again.changed == false + +- name: Remove firewall rule + win_firewall_rule: + name: http + enabled: yes + state: absent + localport: 80 + action: allow + direction: in + protocol: tcp + register: remove_firewall_rule + +- name: Check that removing existing firewall rule succeeds with a change + assert: + that: + - remove_firewall_rule.changed == true + +- name: Remove absent firewall rule + win_firewall_rule: + name: http + enabled: yes + state: absent + localport: 80 + action: allow + direction: in + protocol: tcp + register: remove_absent_firewall_rule + +- name: Check that removing non existing firewall rule succeeds without a change + assert: + that: + - remove_absent_firewall_rule.changed == false + +- name: Add firewall rule + win_firewall_rule: + name: http + enabled: no + state: present + group: application + localport: 80 + action: allow + direction: in + protocol: tcp + register: add_firewall_rule + +- name: Check that creating new firewall rule succeeds with a change + assert: + that: + - add_firewall_rule.changed == true + +- name: Enable all the rules in application group + win_firewall_rule: + group: application + enabled: yes + register: change_firewall_rule + +- name: Check if the rules are enabled in application group + assert: + that: + - change_firewall_rule.changed == true + +- name: Enable all the rules in application group (again) + win_firewall_rule: + group: application + enabled: yes + register: change_firewall_rule + +- name: Check if the rules are enabled without a change + assert: + that: + - change_firewall_rule.changed == false + +- name: Disable all the rules in application group + win_firewall_rule: + group: application + enabled: no + register: change_firewall_rule + +- name: Check if the rules are disabled + assert: + that: + - change_firewall_rule.changed == true + +- name: Disable all the rules in application group (again) + win_firewall_rule: + group: application + enabled: no + register: change_firewall_rule + +- name: Check if the rules are disabled without a change + assert: + that: + - change_firewall_rule.changed == false + +- name: Remove firewall rule + win_firewall_rule: + name: http + enabled: no + state: absent + localport: 80 + action: allow + group: application + direction: in + protocol: tcp + register: remove_firewall_rule + +- name: Check that removing existing firewall rule succeeds with a change + assert: + that: + - remove_firewall_rule.changed == true + +- name: Add firewall rule + win_firewall_rule: + name: http + enabled: yes + state: present + localport: 80 + action: allow + direction: in + protocol: tcp + +- name: Change firewall rule + win_firewall_rule: + name: http + enabled: yes + state: present + localport: 80 + action: block + direction: in + protocol: tcp + register: change_firewall_rule + +- name: Check that changing firewall rule succeeds + assert: + that: + - change_firewall_rule.changed == true + +- name: Disable firewall rule + win_firewall_rule: + name: http + enabled: no + +- name: Get the actual values from the changed firewall rule + ansible.windows.win_shell: '(New-Object -ComObject HNetCfg.FwPolicy2).Rules | Where-Object { $_.Name -eq "http" } | Foreach-Object { $_.LocalPorts; $_.Enabled; $_.Action; $_.Direction; $_.Protocol }' + register: firewall_rule_actual + +- name: Ensure that disabling the rule did not change the previous values + assert: + that: + - "firewall_rule_actual.stdout_lines[0] == '80'" # LocalPorts = 80 + - "firewall_rule_actual.stdout_lines[1] == 'False'" # Enabled = False + - "firewall_rule_actual.stdout_lines[2] == '0'" # Action = block + - "firewall_rule_actual.stdout_lines[3] == '1'" # Direction = in + - "firewall_rule_actual.stdout_lines[4] == '6'" # Protocol = tcp + +- name: Add firewall rule when remoteip is range + win_firewall_rule: + name: http + enabled: yes + state: present + localport: 80 + remoteip: 192.168.0.1-192.168.0.5 + action: allow + direction: in + protocol: tcp + +- name: Add same firewall rule when remoteip is range (again) + win_firewall_rule: + name: http + enabled: yes + state: present + localport: 80 + remoteip: 192.168.0.1-192.168.0.5 + action: allow + direction: in + protocol: tcp + register: add_firewall_rule_with_range_remoteip_again + +- name: Check that creating same firewall rule when remoteip is range succeeds without a change + assert: + that: + - add_firewall_rule_with_range_remoteip_again.changed == false + +- name: Add firewall rule when remoteip in CIDR notation + win_firewall_rule: + name: http + enabled: yes + state: present + localport: 80 + remoteip: 192.168.0.0/24 + action: allow + direction: in + protocol: tcp + +- name: Add same firewall rule when remoteip in CIDR notation (again) + win_firewall_rule: + name: http + enabled: yes + state: present + localport: 80 + remoteip: 192.168.0.0/24 + action: allow + direction: in + protocol: tcp + register: add_firewall_rule_with_cidr_remoteip_again + +- name: Check that creating same firewall rule succeeds without a change when remoteip in CIDR notation + assert: + that: + - add_firewall_rule_with_cidr_remoteip_again.changed == false + +- name: Add firewall rule when remoteip contains a netmask + win_firewall_rule: + name: http + enabled: yes + state: present + localport: 80 + remoteip: 192.168.1.0/255.255.255.0 + action: allow + direction: in + protocol: tcp + +- name: Add same firewall rule when remoteip contains a netmask (again) + win_firewall_rule: + name: http + enabled: yes + state: present + localport: 80 + remoteip: 192.168.1.0/255.255.255.0 + action: allow + direction: in + protocol: tcp + register: add_firewall_rule_remoteip_contains_netmask_again + +- name: Check that creating same firewall rule succeeds without a change when remoteip contains a netmask + assert: + that: + - add_firewall_rule_remoteip_contains_netmask_again.changed == false + +- name: Add firewall rule when remoteip is IPv4 + win_firewall_rule: + name: http + enabled: yes + state: present + localport: 80 + remoteip: 192.168.0.1 + action: allow + direction: in + protocol: tcp + +- name: Add same firewall rule when remoteip is IPv4 (again) + win_firewall_rule: + name: http + enabled: yes + state: present + localport: 80 + remoteip: 192.168.0.1 + action: allow + direction: in + protocol: tcp + register: add_firewall_rule_with_ipv4_remoteip_again + +- name: Check that creating same firewall rule when remoteip is IPv4 succeeds without a change + assert: + that: + - add_firewall_rule_with_ipv4_remoteip_again.changed == false + +- name: Add firewall rule when remoteip contains a netmask + win_firewall_rule: + name: http + enabled: yes + state: present + localport: 80 + remoteip: 192.168.2.0/255.255.255.0 + action: allow + direction: in + protocol: tcp + +- name: Add same firewall rule when remoteip in CIDR notation + win_firewall_rule: + name: http + enabled: yes + state: present + localport: 80 + remoteip: 192.168.2.0/24 + action: allow + direction: in + protocol: tcp + register: add_same_firewall_rule_with_cidr_remoteip + +- name: Check that creating same firewall rule succeeds without a change when remoteip contains a netmask or CIDR + assert: + that: + - add_same_firewall_rule_with_cidr_remoteip.changed == false + +- name: Add firewall rule with multiple ports + win_firewall_rule: + name: http + enabled: yes + state: present + localport: '80,81' + action: allow + direction: in + protocol: tcp + register: add_firewall_rule_with_multiple_ports + +- name: Check that creating firewall rule with multiple ports succeeds with a change + assert: + that: + - add_firewall_rule_with_multiple_ports.changed == true + +- name: Add firewall rule with interface types in string format + win_firewall_rule: + name: http + enabled: yes + state: present + localport: 80 + action: allow + direction: in + protocol: tcp + interfacetypes: 'ras,lan,wireless' + register: add_firewall_rule_with_string_interface_types + +- name: Check that creating firewall rule with interface types in string format succeeds with a change + assert: + that: + - add_firewall_rule_with_string_interface_types.changed == true + +- name: Add firewall rule with interface types in list format + win_firewall_rule: + name: http + enabled: yes + state: present + localport: 80 + action: allow + direction: in + protocol: tcp + interfacetypes: [ras, lan] + register: add_firewall_rule_with_list_interface_types + +- name: Check that creating firewall rule with interface types in list format succeeds with a change + assert: + that: + - add_firewall_rule_with_list_interface_types.changed == true + +- name: Add firewall rule with interface type 'any' + win_firewall_rule: + name: http + enabled: yes + state: present + localport: 80 + action: allow + direction: in + protocol: tcp + interfacetypes: any + register: add_firewall_rule_with_interface_type_any + +- name: Check that creating firewall rule with interface type 'any' succeeds with a change + assert: + that: + - add_firewall_rule_with_interface_type_any.changed == true + +- name: Add firewall rule with edge traversal option 'deferapp' + win_firewall_rule: + name: http + enabled: yes + state: present + localport: 80 + action: allow + direction: in + protocol: tcp + edge: deferapp + register: add_firewall_rule_with_edge_traversal + +# Setup action creates ansible_distribution_version variable +- ansible.windows.setup: + +- name: Check that creating firewall rule with enge traversal option 'deferapp' succeeds with a change + assert: + that: + - add_firewall_rule_with_edge_traversal.changed == true + # Works on windows >= Windows 7/Windows Server 2008 R2 + when: ansible_distribution_version is version('6.1', '>=') + +- name: Add firewall rule with 'authenticate' secure flag + win_firewall_rule: + name: http + enabled: yes + state: present + localport: 80 + action: allow + direction: in + protocol: tcp + security: authenticate + register: add_firewall_rule_with_secure_flags + +- name: Check that creating firewall rule with secure flag 'authenticate' succeeds with a change + assert: + that: + - add_firewall_rule_with_secure_flags.changed == true + # Works on windows >= Windows 8/Windows Server 2012 + when: ansible_distribution_version is version('6.2', '>=') + +- name: Add firewall rule with profiles in string format + win_firewall_rule: + name: http + enabled: yes + state: present + localport: 80 + action: allow + direction: in + protocol: tcp + profiles: 'domain,public' + register: add_firewall_rule_with_string_profiles + +- name: Check that creating firewall rule with profiles in string format succeeds with a change + assert: + that: + - add_firewall_rule_with_string_profiles.changed == true + +- name: Set firewall rule profile back to 'all' + win_firewall_rule: + name: http + enabled: yes + state: present + localport: 80 + action: allow + direction: in + protocol: tcp + profiles: [Domain, Public, Private] + register: add_firewall_rule_with_string_profiles + +- name: Check that setting firewall rule profile back to 'all' succeeds with a change + assert: + that: + - add_firewall_rule_with_string_profiles.changed == true + +- name: Add firewall rule with profiles in list format + win_firewall_rule: + name: http + enabled: yes + state: present + localport: 80 + action: allow + direction: in + protocol: tcp + profiles: [Domain, Private] + register: add_firewall_rule_with_list_profiles + +- name: Check that creating firewall rule with profiles in list format succeeds with a change + assert: + that: + - add_firewall_rule_with_list_profiles.changed == true + +# Test for variable expansion in the path +- name: Add rule with path that needs to be expanded + win_firewall_rule: + name: VarExpansionTest + enabled: yes + state: present + action: allow + direction: in + protocol: tcp + program: '%SystemRoot%\system32\svchost.exe' + +- name: Add same rule with path that needs to be expanded + win_firewall_rule: + name: VarExpansionTest + enabled: yes + state: present + action: allow + direction: in + protocol: tcp + program: '%SystemRoot%\system32\svchost.exe' + register: add_firewall_rule_with_var_expand_path + +- name: Check that creating same firewall rule with expanded vars identified + assert: + that: + - add_firewall_rule_with_var_expand_path.changed == false + +- name: Add firewall rule for application group + win_firewall_rule: + name: Rule for application group + enabled: yes + state: present + localport: 80 + action: allow + direction: in + protocol: tcp + group: application + register: add_firewall_rule_with_group + +- name: Check that creating firewall rule for application group succeeds with a change + assert: + that: + - add_firewall_rule_with_group.changed == true + +# Test icmptypecode +- name: Add rule with icmptypecode + win_firewall_rule: + name: icmptest + enabled: yes + state: present + action: allow + direction: in + protocol: icmpv4 + icmp_type_code: '8:*' + register: add_firewall_rule_with_icmptypecode + +- name: Check that creating same firewall rule with expanded vars identified + assert: + that: + - add_firewall_rule_with_icmptypecode.changed == true + +- name: Remove rule with icmptypecode + win_firewall_rule: + name: icmptest + enabled: yes + state: absent + action: allow + direction: in + protocol: icmpv4 + icmp_type_code: '8:*' + register: remove_firewall_rule_with_icmptypecode + +- name: Check that removing same firewall rule with expanded vars identified + assert: + that: + - remove_firewall_rule_with_icmptypecode.changed == true + +# test for application name / program changes to any (and if they null the property) +# ----------------------------------------------------------------------------- +- name: Add rule with an accociated program + win_firewall_rule: + name: ApplicationToAnyTest + enabled: true + state: present + action: allow + direction: in + protocol: tcp + program: '%SystemRoot%\system32\svchost.exe' + register: add_firewall_rule_with_program + +- name: Check that creating firewall rule succeeds with a change + ansible.builtin.assert: + that: + - add_firewall_rule_with_program.changed == true + +- name: Add same rule with program set to any + win_firewall_rule: + name: ApplicationToAnyTest + enabled: true + state: present + action: allow + direction: in + protocol: tcp + program: any + register: change_firewall_rule_with_program + +- name: Check that changing firewall rule succeeds with a change + ansible.builtin.assert: + that: + - change_firewall_rule_with_program.changed == true + +- name: Get the actual values from the changed firewall rule and check if ApplicationName is null + ansible.windows.win_shell: >- + ((New-Object -ComObject HNetCfg.FwPolicy2).Rules | Where-Object { $_.Name -eq "ApplicationToAnyTest" } | Foreach-Object { $_.ApplicationName }) -eq $null + register: firewall_rule_actual + failed_when: 'firewall_rule_actual.stdout_lines[0] != "True"' +# ----------------------------------------------------------------------------- diff --git a/ansible_collections/community/windows/tests/integration/targets/win_format/aliases b/ansible_collections/community/windows/tests/integration/targets/win_format/aliases new file mode 100644 index 000000000..4f4664b68 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_format/aliases @@ -0,0 +1 @@ +shippable/windows/group5 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_format/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_format/meta/main.yml new file mode 100644 index 000000000..9f37e96cd --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_format/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_remote_tmp_dir diff --git a/ansible_collections/community/windows/tests/integration/targets/win_format/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_format/tasks/main.yml new file mode 100644 index 000000000..5ea27a6f0 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_format/tasks/main.yml @@ -0,0 +1,7 @@ +--- +- name: Check if Format-Volume is supported + ansible.windows.win_shell: if (Get-Command -Name Format-Volume -ErrorAction SilentlyContinue) { $true } else { $false } + register: module_present + +- include: pre_test.yml + when: module_present.stdout | trim | bool diff --git a/ansible_collections/community/windows/tests/integration/targets/win_format/tasks/pre_test.yml b/ansible_collections/community/windows/tests/integration/targets/win_format/tasks/pre_test.yml new file mode 100644 index 000000000..a29a47bbe --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_format/tasks/pre_test.yml @@ -0,0 +1,21 @@ +--- +- set_fact: + AnsibleVhdx: '{{ remote_tmp_dir }}\AnsiblePart.vhdx' + +- name: Copy VHDX scripts + ansible.windows.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 + ansible.windows.win_command: diskpart.exe /s {{ remote_tmp_dir }}\partition_creation_script.txt + +- name: Run tests + block: + - include: tests.yml + always: + - name: Detach disk + ansible.windows.win_command: diskpart.exe /s {{ remote_tmp_dir }}\partition_deletion_script.txt diff --git a/ansible_collections/community/windows/tests/integration/targets/win_format/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_format/tasks/tests.yml new file mode 100644 index 000000000..1383c6f9c --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_format/tasks/tests.yml @@ -0,0 +1,182 @@ +--- +- ansible.windows.win_shell: $AnsiPart = Get-Partition -DriveLetter T; $AnsiVol = Get-Volume -DriveLetter T; "$($AnsiPart.Size),$($AnsiVol.Size)" + register: shell_result + +- name: Assert volume size is 0 for pristine volume + assert: + that: + - shell_result.stdout | trim == "2096037888,0" + +- name: Get partition access path + ansible.windows.win_shell: (Get-Partition -DriveLetter T).AccessPaths[1] + register: shell_partition_result + +- name: Try to format using mutually exclusive parameters + win_format: + drive_letter: T + path: "{{ shell_partition_result.stdout | trim }}" + register: format_mutex_result + ignore_errors: True + +- assert: + that: + - format_mutex_result is failed + - 'format_mutex_result.msg == "parameters are mutually exclusive: drive_letter, path, label"' + +- name: Fully format volume and assign label (check) + win_format: + drive_letter: T + new_label: Formatted + full: True + allocation_unit_size: 8192 + register: format_result_check + check_mode: True + +- ansible.windows.win_shell: $AnsiPart = Get-Partition -DriveLetter T; $AnsiVol = Get-Volume -DriveLetter T; "$($AnsiPart.Size),$($AnsiVol.Size),$($AnsiVol.FileSystemLabel),$((Get-CimInstance -ClassName Win32_Volume -Filter "DriveLetter = 'T:'" -Property BlockSize).BlockSize)" + register: formatted_value_result_check + +- name: Fully format volume and assign label + win_format: + drive_letter: T + new_label: Formatted + full: True + allocation_unit_size: 8192 + register: format_result + +- ansible.windows.win_shell: $AnsiPart = Get-Partition -DriveLetter T; $AnsiVol = Get-Volume -DriveLetter T; "$($AnsiPart.Size),$($AnsiVol.Size),$($AnsiVol.FileSystemLabel),$((Get-CimInstance -ClassName Win32_Volume -Filter "DriveLetter = 'T:'" -Property BlockSize).BlockSize)" + register: formatted_value_result + +- assert: + that: + - format_result_check is changed + - format_result is changed + - formatted_value_result_check.stdout | trim == "2096037888,0,," + - formatted_value_result.stdout | trim == "2096037888,2096029696,Formatted,8192" + +- name: Format NTFS volume with integrity streams enabled + win_format: + path: "{{ shell_partition_result.stdout | trim }}" + file_system: ntfs + integrity_streams: True + ignore_errors: True + register: ntfs_integrity_streams + +- assert: + that: + - ntfs_integrity_streams is failed + - 'ntfs_integrity_streams.msg == "Integrity streams can be enabled only on ReFS volumes. You specified: ntfs"' + +- name: Format volume (require force_format for specifying different file system) + win_format: + path: "{{ shell_partition_result.stdout | trim }}" + file_system: fat32 + ignore_errors: True + register: require_force_format + +- assert: + that: + - require_force_format is failed + - 'require_force_format.msg == "Force format must be specified since target file system: fat32 is different from the current file system of the volume: ntfs"' + +- name: Format volume (forced) (check) + win_format: + path: "{{ shell_partition_result.stdout | trim }}" + file_system: refs + force: True + check_mode: True + ignore_errors: True + register: not_pristine_forced_check + +- name: Format volume (forced) + win_format: + path: "{{ shell_partition_result.stdout | trim }}" + file_system: refs + force: True + register: not_pristine_forced + +- name: Format volume (forced) (idempotence will not work) + win_format: + path: "{{ shell_partition_result.stdout | trim }}" + file_system: refs + force: True + register: not_pristine_forced_idem_fails + +- name: Format volume (idempotence) + win_format: + path: "{{ shell_partition_result.stdout | trim }}" + file_system: refs + register: not_pristine_forced_idem + +- assert: + that: + - not_pristine_forced_check is changed + - not_pristine_forced is changed + - not_pristine_forced_idem_fails is changed + - not_pristine_forced_idem is not changed + +- name: Add a file + ansible.windows.win_file: + path: T:\path\to\directory + state: directory + +- name: Format volume with file inside without force and same fs + win_format: + path: "{{ shell_partition_result.stdout | trim }}" + register: format_volume_without_force_same_fs + +- name: Format volume (forced) - to test case for files existing and a different fs + win_format: + path: "{{ shell_partition_result.stdout | trim }}" + file_system: ntfs + force: True + +- name: Add a file + ansible.windows.win_file: + path: T:\path\to\directory + state: directory + register: add_file_to_volume + +- name: Format volume with file inside without force + win_format: + path: "{{ shell_partition_result.stdout | trim }}" + file_system: refs + register: format_volume_without_force + ignore_errors: True + +- name: Format volume with file inside with force + win_format: + path: "{{ shell_partition_result.stdout | trim }}" + force: True + register: format_volume_with_force + +- assert: + that: + - add_file_to_volume is changed + - format_volume_without_force is failed + - format_volume_without_force_same_fs is not changed + - 'format_volume_without_force.msg == "Force format must be specified to format non-pristine volumes"' + - format_volume_with_force is changed + +- name: Reformat using different alu without force format + win_format: + path: "{{ shell_partition_result.stdout | trim }}" + allocation_unit_size: 8192 + file_system: ntfs + register: reformat_using_alu_without_force + ignore_errors: True + +- assert: + that: + - reformat_using_alu_without_force is failed + +- name: Reformat using different alu using force format + win_format: + path: "{{ shell_partition_result.stdout | trim }}" + allocation_unit_size: 8192 + file_system: ntfs + force: True + register: reformat_using_alu_with_force + +- assert: + that: + - reformat_using_alu_with_force is changed diff --git a/ansible_collections/community/windows/tests/integration/targets/win_format/templates/partition_creation_script.j2 b/ansible_collections/community/windows/tests/integration/targets/win_format/templates/partition_creation_script.j2 new file mode 100644 index 000000000..8e47fda95 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_format/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/ansible_collections/community/windows/tests/integration/targets/win_format/templates/partition_deletion_script.j2 b/ansible_collections/community/windows/tests/integration/targets/win_format/templates/partition_deletion_script.j2 new file mode 100644 index 000000000..c2be9cd14 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_format/templates/partition_deletion_script.j2 @@ -0,0 +1,3 @@ +select vdisk file="{{ AnsibleVhdx }}" + +detach vdisk diff --git a/ansible_collections/community/windows/tests/integration/targets/win_hosts/aliases b/ansible_collections/community/windows/tests/integration/targets/win_hosts/aliases new file mode 100644 index 000000000..4cd27b3cb --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_hosts/aliases @@ -0,0 +1 @@ +shippable/windows/group1 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_hosts/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_hosts/defaults/main.yml new file mode 100644 index 000000000..c6270216d --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/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/ansible_collections/community/windows/tests/integration/targets/win_hosts/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_hosts/meta/main.yml new file mode 100644 index 000000000..9f37e96cd --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_hosts/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_remote_tmp_dir diff --git a/ansible_collections/community/windows/tests/integration/targets/win_hosts/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_hosts/tasks/main.yml new file mode 100644 index 000000000..02a8b873e --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_hosts/tasks/main.yml @@ -0,0 +1,17 @@ +--- +- name: take a copy of the original hosts file + ansible.windows.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 + ansible.windows.win_copy: + src: '{{ remote_tmp_dir }}\hosts' + dest: C:\Windows\System32\drivers\etc\hosts + remote_src: yes diff --git a/ansible_collections/community/windows/tests/integration/targets/win_hosts/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_hosts/tasks/tests.yml new file mode 100644 index 000000000..5ced7ba26 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/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 + ansible.windows.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 + ansible.windows.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 + ansible.windows.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 + ansible.windows.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 + ansible.windows.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 + ansible.windows.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 + ansible.windows.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/ansible_collections/community/windows/tests/integration/targets/win_hotfix/aliases b/ansible_collections/community/windows/tests/integration/targets/win_hotfix/aliases new file mode 100644 index 000000000..11addc63b --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_hotfix/aliases @@ -0,0 +1,2 @@ +shippable/windows/group4 +unstable diff --git a/ansible_collections/community/windows/tests/integration/targets/win_hotfix/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_hotfix/defaults/main.yml new file mode 100644 index 000000000..22edea7c1 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_hotfix/defaults/main.yml @@ -0,0 +1,13 @@ +--- +# these hotfixes, are for Hyper-V, there may be a chance the system already has them +# but in most cases for our CI purposes they wouldn't be present +test_win_hotfix_good_url: https://ansible-ci-files.s3.amazonaws.com/test/integration/targets/win_hotfix/windows8.1-kb3027108-v2-x64_66366c7be2d64d83b63cac42bc40c0a3c01bc70d.msu +test_win_hotfix_reboot_url: https://ansible-ci-files.s3.amazonaws.com/test/integration/targets/win_hotfix/windows8.1-kb2913659-v2-x64_963a4d890c9ff9cc83a97cf54305de6451038ba4.msu +test_win_hotfix_bad_url: https://ansible-ci-files.s3.amazonaws.com/test/integration/targets/win_hotfix/windows8-rt-kb3172729-x64_69cab4c7785b1faa3fc450f32bed4873d53bb96f.msu +test_win_hotfix_path: C:\ansible\win_hotfix + +test_win_hotfix_kb: KB3027108 +test_win_hotfix_identifier: Package_for_KB3027108~31bf3856ad364e35~amd64~~6.3.2.0 + +test_win_hotfix_reboot_kb: KB2913659 +test_win_hotfix_reboot_identifier: Package_for_KB2913659~31bf3856ad364e35~amd64~~6.3.2.0 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_hotfix/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_hotfix/tasks/main.yml new file mode 100644 index 000000000..47b2d3056 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_hotfix/tasks/main.yml @@ -0,0 +1,54 @@ +--- +- name: filter servers that can support DISM + ansible.windows.win_command: powershell.exe "Import-Module -Name DISM" + register: eligable_servers + ignore_errors: True + +- name: fail to run module on servers that don't support DISM + win_hotfix: + path: fake + state: present + register: fail_no_dism + failed_when: fail_no_dism.msg != 'The DISM PS module needs to be installed, this can be done through the windows-adk chocolately package' + when: eligable_servers.rc != 0 + +- name: run tests on hosts that support DISM + include_tasks: tests.yml + when: eligable_servers.rc == 0 + +- name: set output to true if running Server 2012 R2 + ansible.windows.win_command: powershell.exe "$version = [Environment]::OSVersion.Version; if ($version.Major -eq 6 -and $version.Minor -eq 3) { 'true' } else { 'false' }" + register: test_hotfix + +- block: + - name: ensure hotfixes are uninstalled before tests + win_hotfix: + hotfix_identifier: '{{item}}' + state: absent + register: pre_uninstall + with_items: + - '{{test_win_hotfix_identifier}}' + - '{{test_win_hotfix_reboot_identifier}}' + + - name: reboot after pre test uninstall if required + ansible.windows.win_reboot: + when: pre_uninstall.results[0].reboot_required == True or pre_uninstall.results[1].reboot_required == True + + - name: run actual hotfix tests on Server 2012 R2 only + include_tasks: tests_2012R2.yml + + always: + - name: ensure hotfixes are uninstalled after tests + win_hotfix: + hotfix_identifier: '{{item}}' + state: absent + register: post_uninstall + with_items: + - '{{test_win_hotfix_identifier}}' + - '{{test_win_hotfix_reboot_identifier}}' + + - name: reboot after post test uninstall if required + ansible.windows.win_reboot: + when: post_uninstall.results[0].reboot_required == True or post_uninstall.results[1].reboot_required == True + + when: test_hotfix.stdout_lines[0] == "true" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_hotfix/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_hotfix/tasks/tests.yml new file mode 100644 index 000000000..8e7a7df37 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_hotfix/tasks/tests.yml @@ -0,0 +1,35 @@ +# only basic tests, doesn't actually install/uninstall and hotfixes +--- +- name: fail when source isn't set + win_hotfix: + state: present + register: fail_no_source + failed_when: fail_no_source.msg != 'source must be set when state=present' + +- name: fail when identifier or kb isn't set on absent + win_hotfix: + state: absent + register: fail_no_key + failed_when: fail_no_key.msg != 'either hotfix_identifier or hotfix_kb needs to be set when state=absent' + +- name: remove an identifier that isn't installed + win_hotfix: + hotfix_identifier: fake~identifier + state: absent + register: remove_missing_hotfix_identifier + +- name: assert remove an identifier that isn't installed + assert: + that: + - remove_missing_hotfix_identifier is not changed + +- name: remove a kb that isn't installed + win_hotfix: + hotfix_kb: KB123456 + state: absent + register: remove_missing_hotfix_kb + +- name: assert remove a kb that isn't installed + assert: + that: + - remove_missing_hotfix_kb is not changed diff --git a/ansible_collections/community/windows/tests/integration/targets/win_hotfix/tasks/tests_2012R2.yml b/ansible_collections/community/windows/tests/integration/targets/win_hotfix/tasks/tests_2012R2.yml new file mode 100644 index 000000000..14ff38ec5 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_hotfix/tasks/tests_2012R2.yml @@ -0,0 +1,265 @@ +--- +- name: create test staging folder + ansible.windows.win_file: + path: '{{test_win_hotfix_path}}' + state: directory + +- name: download hotfix + ansible.windows.win_get_url: + url: '{{test_win_hotfix_good_url}}' + dest: '{{test_win_hotfix_path}}\good.msu' + register: download_res + until: download_res is successful + retries: 3 + delay: 5 + +- name: download reboot hotfix + ansible.windows.win_get_url: + url: '{{test_win_hotfix_reboot_url}}' + dest: '{{test_win_hotfix_path}}\reboot.msu' + register: download_res + until: download_res is successful + retries: 3 + delay: 5 + +- name: download bad hotfix + ansible.windows.win_get_url: + url: '{{test_win_hotfix_bad_url}}' + dest: '{{test_win_hotfix_path}}\bad.msu' + register: download_res + until: download_res is successful + retries: 3 + delay: 5 + +- name: fail install install hotfix where kb doesn't match + win_hotfix: + hotfix_kb: KB0000000 + source: '{{test_win_hotfix_path}}\good.msu' + state: present + register: fail_install_invalid_kb + failed_when: fail_install_invalid_kb.msg != 'the hotfix KB KB0000000 does not match with the source msu KB ' + test_win_hotfix_kb + ', please omit or specify the correct KB to continue' + +- name: fail install install hotfix where identifier doesn't match + win_hotfix: + hotfix_identifier: invalid + source: '{{test_win_hotfix_path}}\good.msu' + state: present + register: fail_install_invalid_identifier + failed_when: fail_install_invalid_identifier.msg != 'the hotfix identifier invalid does not match with the source msu identifier ' + test_win_hotfix_identifier + ', please omit or specify the correct identifier to continue' + +- name: fail install not applicable hotfix + win_hotfix: + source: '{{test_win_hotfix_path}}\bad.msu' + state: present + register: fail_install_not_applicable + failed_when: fail_install_not_applicable.msg != 'hotfix package is not applicable for this server' + +- name: install hotfix check + win_hotfix: + source: '{{test_win_hotfix_path}}\good.msu' + state: present + register: install_hotfix_check + check_mode: yes + +- name: get result of install hotfix check + ansible.windows.win_command: powershell.exe Get-Hotfix -Id {{test_win_hotfix_kb}} + register: install_hotfix_actual_check + ignore_errors: True + +- name: assert install hotfix check + assert: + that: + - install_hotfix_check is changed + - install_hotfix_check.kb == test_win_hotfix_kb + - install_hotfix_check.identifier == test_win_hotfix_identifier + - install_hotfix_actual_check.rc != 0 + +- name: install hotfix + win_hotfix: + source: '{{test_win_hotfix_path}}\good.msu' + state: present + register: install_hotfix + +- name: get result of install hotfix + ansible.windows.win_command: powershell.exe Get-Hotfix -Id {{test_win_hotfix_kb}} + register: install_hotfix_actual + +- name: assert install hotfix + assert: + that: + - install_hotfix is changed + - install_hotfix.kb == test_win_hotfix_kb + - install_hotfix.identifier == test_win_hotfix_identifier + - install_hotfix.reboot_required == False + - install_hotfix_actual.rc == 0 + +- name: install hotfix again + win_hotfix: + source: '{{test_win_hotfix_path}}\good.msu' + state: present + register: install_hotfix_again + +- name: assert install hotfix again + assert: + that: + - install_hotfix_again is not changed + - install_hotfix_again.kb == test_win_hotfix_kb + - install_hotfix_again.identifier == test_win_hotfix_identifier + - install_hotfix_again.reboot_required == False + +- name: uninstall hotfix check + win_hotfix: + hotfix_identifier: '{{test_win_hotfix_identifier}}' + state: absent + register: uninstall_hotfix_check + check_mode: yes + +- name: get result of uninstall hotfix check + ansible.windows.win_command: powershell.exe Get-Hotfix -Id {{test_win_hotfix_kb}} + register: uninstall_hotfix_actual_check + +- name: assert uninstall hotfix check + assert: + that: + - uninstall_hotfix_check is changed + - uninstall_hotfix_check.kb == test_win_hotfix_kb + - uninstall_hotfix_check.identifier == test_win_hotfix_identifier + - uninstall_hotfix_actual_check.rc == 0 + +- name: uninstall hotfix + win_hotfix: + hotfix_identifier: '{{test_win_hotfix_identifier}}' + state: absent + register: uninstall_hotfix + +- name: get result of uninstall hotfix + ansible.windows.win_command: powershell.exe Get-Hotfix -Id {{test_win_hotfix_kb}} + register: uninstall_hotfix_actual + ignore_errors: True + +- name: assert uninstall hotfix + assert: + that: + - uninstall_hotfix is changed + - uninstall_hotfix.kb == test_win_hotfix_kb + - uninstall_hotfix.identifier == test_win_hotfix_identifier + - uninstall_hotfix.reboot_required == False + - uninstall_hotfix_actual.rc != 0 + +- name: uninstall hotfix again + win_hotfix: + hotfix_identifier: '{{test_win_hotfix_identifier}}' + state: absent + register: uninstall_hotfix_again + +- name: assert uninstall hotfix again + assert: + that: + - uninstall_hotfix_again is not changed + - uninstall_hotfix_again.reboot_required == False + +- name: install reboot hotfix + win_hotfix: + hotfix_kb: '{{test_win_hotfix_reboot_kb}}' + source: '{{test_win_hotfix_path}}\reboot.msu' + state: present + register: install_reboot_hotfix + +- name: get result of install reboot hotfix + ansible.windows.win_command: powershell.exe Get-Hotfix -Id {{test_win_hotfix_reboot_kb}} + register: install_hotfix_reboot_actual + +- name: assert install reboot hotfix + assert: + that: + - install_reboot_hotfix is changed + - install_reboot_hotfix.kb == test_win_hotfix_reboot_kb + - install_reboot_hotfix.identifier == test_win_hotfix_reboot_identifier + - install_reboot_hotfix.reboot_required == True + - install_hotfix_reboot_actual.rc == 0 + +- name: run install reboot again before rebooting + win_hotfix: + source: '{{test_win_hotfix_path}}\reboot.msu' + state: present + register: install_before_rebooting + +- name: assert install reboot again before rebooting + assert: + that: + - install_before_rebooting is not changed + - install_before_rebooting.reboot_required == True + +- ansible.windows.win_reboot: + +- name: install reboot hotfix again + win_hotfix: + hotfix_identifier: '{{test_win_hotfix_reboot_identifier}}' + source: '{{test_win_hotfix_path}}\reboot.msu' + state: present + register: install_reboot_hotfix_again + +- name: assert install reboot hotfix again + assert: + that: + - install_reboot_hotfix_again is not changed + - install_reboot_hotfix_again.reboot_required == False + +- name: uninstall hotfix with kb check + win_hotfix: + hotfix_kb: '{{test_win_hotfix_reboot_kb}}' + state: absent + register: uninstall_hotfix_kb_check + check_mode: yes + +- name: get result of uninstall hotfix with kb check + ansible.windows.win_command: powershell.exe Get-Hotfix -Id {{test_win_hotfix_reboot_kb}} + register: uninstall_hotfix_kb_actual_check + +- name: assert uninstall hotfix with kb check + assert: + that: + - uninstall_hotfix_kb_check is changed + - uninstall_hotfix_kb_check.kb == test_win_hotfix_reboot_kb + - uninstall_hotfix_kb_check.identifier == test_win_hotfix_reboot_identifier + - uninstall_hotfix_kb_check.reboot_required == False + - uninstall_hotfix_kb_actual_check.rc == 0 + +- name: uninstall hotfix with kb + win_hotfix: + hotfix_kb: '{{test_win_hotfix_reboot_kb}}' + state: absent + register: uninstall_hotfix_kb + +- name: get result of uninstall hotfix with kb + ansible.windows.win_command: powershell.exe Get-Hotfix -Id {{test_win_hotfix_kb}} + register: uninstall_hotfix_kb_actual + ignore_errors: True + +- name: assert uninstall hotfix with kb + assert: + that: + - uninstall_hotfix_kb is changed + - uninstall_hotfix_kb.kb == test_win_hotfix_reboot_kb + - uninstall_hotfix_kb.identifier == test_win_hotfix_reboot_identifier + - uninstall_hotfix_kb.reboot_required == True + - uninstall_hotfix_kb_actual.rc != 0 + +- ansible.windows.win_reboot: + +- name: uninstall hotfix with kb again + win_hotfix: + hotfix_kb: '{{test_win_hotfix_reboot_kb}}' + state: absent + register: uninstall_hotfix_kb_again + +- name: assert uninstall hotfix with kb again + assert: + that: + - uninstall_hotfix_kb_again is not changed + - uninstall_hotfix_kb_again.reboot_required == False + +- name: remove test staging folder + ansible.windows.win_file: + path: '{{test_win_hotfix_path}}' + state: absent diff --git a/ansible_collections/community/windows/tests/integration/targets/win_http_proxy/aliases b/ansible_collections/community/windows/tests/integration/targets/win_http_proxy/aliases new file mode 100644 index 000000000..4cd27b3cb --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_http_proxy/aliases @@ -0,0 +1 @@ +shippable/windows/group1 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_http_proxy/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_http_proxy/tasks/main.yml new file mode 100644 index 000000000..5da9aa7fe --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_http_proxy/tasks/main.yml @@ -0,0 +1,14 @@ +--- +- name: make sure we start the tests with no proxy set + win_http_proxy: + +- block: + - name: run tests + include_tasks: tests.yml + + always: + - name: remove any explicit proxy settings + win_http_proxy: + + - name: reset WinINet proxy settings + win_inet_proxy: diff --git a/ansible_collections/community/windows/tests/integration/targets/win_http_proxy/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_http_proxy/tasks/tests.yml new file mode 100644 index 000000000..04a763d08 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_http_proxy/tasks/tests.yml @@ -0,0 +1,265 @@ +--- +- name: ensure we fail when proxy is not set with bypass + win_http_proxy: + bypass: abc + register: fail_bypass + failed_when: 'fail_bypass.msg != "missing parameter(s) required by ''bypass'': proxy"' + +- name: ensure we fail when proxy and source is set + win_http_proxy: + proxy: proxy + source: ie + register: fail_source + failed_when: 'fail_source.msg != "parameters are mutually exclusive: proxy, source"' + +- name: ensure we fail if an invalid protocol is specified + win_http_proxy: + proxy: + fail1: fail + fail2: fail + register: fail_protocol + failed_when: 'fail_protocol.msg != "Invalid keys found in proxy: fail1, fail2. Valid keys are http, https, ftp, socks."' + +# WinHTTP does not validate on set, this ensures the module checks and revert any failed attempts at setting the proxy +# FIXME: Only certain hosts seem to have a strict winhttp definition, we can't run this in CI for now +#- name: ensure we fail if invalid value is set +# win_http_proxy: +# proxy: fake=proxy +# register: fail_invalid +# failed_when: fail_invalid.msg != "Unknown error when trying to set proxy 'fake=proxy' or bypass ''" +# +#- name: check proxy is still set to Direct access +# ansible.windows.win_command: netsh winhttp show proxy +# register: fail_invalid_actual +# failed_when: fail_invalid_actual.stdout_lines[3]|trim != "Direct access (no proxy server)." + +- name: set a proxy using a string (check) + win_http_proxy: + proxy: proxyhost + register: proxy_str_check + check_mode: True + +- name: get result of set a proxy using a string (check) + ansible.windows.win_command: netsh winhttp show proxy + register: proxy_str_actual_check + +- name: assert set a proxy using a string (check) + assert: + that: + - proxy_str_check is changed + - proxy_str_actual_check.stdout_lines[3]|trim == "Direct access (no proxy server)." + +- name: set a proxy using a string + win_http_proxy: + proxy: proxyhost + register: proxy_str + +- name: get result of set a proxy using a string + ansible.windows.win_command: netsh winhttp show proxy + register: proxy_str_actual + +- name: assert set a proxy using a string + assert: + that: + - proxy_str is changed + - 'proxy_str_actual.stdout_lines[3]|trim == "Proxy Server(s) : proxyhost"' + - 'proxy_str_actual.stdout_lines[4]|trim == "Bypass List : (none)"' + +- name: set a proxy using a string (idempotent) + win_http_proxy: + proxy: proxyhost + register: proxy_str_again + +- name: assert set a proxy using a string (idempotent) + assert: + that: + - not proxy_str_again is changed + +- name: change a proxy and set bypass (check) + win_http_proxy: + proxy: proxyhost:8080 + bypass: + - abc + - def + - <local> + register: change_proxy_check + check_mode: True + +- name: get result of change a proxy and set bypass (check) + ansible.windows.win_command: netsh winhttp show proxy + register: change_proxy_actual_check + +- name: assert change a proxy and set bypass (check) + assert: + that: + - change_proxy_check is changed + - 'change_proxy_actual_check.stdout_lines[3]|trim == "Proxy Server(s) : proxyhost"' + - 'change_proxy_actual_check.stdout_lines[4]|trim == "Bypass List : (none)"' + +- name: change a proxy and set bypass + win_http_proxy: + proxy: proxyhost:8080 + bypass: + - abc + - def + - <local> + register: change_proxy + +- name: get result of change a proxy and set bypass + ansible.windows.win_command: netsh winhttp show proxy + register: change_proxy_actual + +- name: assert change a proxy and set bypass + assert: + that: + - change_proxy is changed + - 'change_proxy_actual.stdout_lines[3]|trim == "Proxy Server(s) : proxyhost:8080"' + - 'change_proxy_actual.stdout_lines[4]|trim == "Bypass List : abc;def;<local>"' + +- name: change a proxy and set bypass (idempotent) + win_http_proxy: + proxy: proxyhost:8080 + bypass: abc,def,<local> + register: change_proxy_again + +- name: assert change a proxy and set bypass (idempotent) + assert: + that: + - not change_proxy_again is changed + +- name: change bypass list + win_http_proxy: + proxy: proxyhost:8080 + bypass: + - abc + - <-loopback> + register: change_bypass + +- name: get result of change bypass list + ansible.windows.win_command: netsh winhttp show proxy + register: change_bypass_actual + +- name: assert change bypass list + assert: + that: + - change_bypass is changed + - 'change_bypass_actual.stdout_lines[3]|trim == "Proxy Server(s) : proxyhost:8080"' + - 'change_bypass_actual.stdout_lines[4]|trim == "Bypass List : abc;<-loopback>"' + +- name: remove proxy without options (check) + win_http_proxy: + register: remove_proxy_check + check_mode: yes + +- name: get result of remove proxy without options (check) + ansible.windows.win_command: netsh winhttp show proxy + register: remove_proxy_actual_check + +- name: assert remove proxy without options (check) + assert: + that: + - remove_proxy_check is changed + - 'remove_proxy_actual_check.stdout_lines[3]|trim == "Proxy Server(s) : proxyhost:8080"' + - 'remove_proxy_actual_check.stdout_lines[4]|trim == "Bypass List : abc;<-loopback>"' + +- name: remove proxy without options + win_http_proxy: + register: remove_proxy + +- name: get result of remove proxy without options + ansible.windows.win_command: netsh winhttp show proxy + register: remove_proxy_actual + +- name: assert remove proxy without options + assert: + that: + - remove_proxy is changed + - remove_proxy_actual.stdout_lines[3]|trim == "Direct access (no proxy server)." + +- name: remove proxy without options (idempotent) + win_http_proxy: + register: remove_proxy_again + +- name: assert remove proxy without options (idempotent) + assert: + that: + - not remove_proxy_again is changed + +- name: set proxy with dictionary + win_http_proxy: + proxy: + http: proxy:8080 + https: proxy:8443 + ftp: proxy:821 + socks: proxy:888 + register: set_dict + +- name: get result of set proxy with dictionary + ansible.windows.win_command: netsh winhttp show proxy + register: set_dict_actual + +- name: assert set proxy with dictionary + assert: + that: + - set_dict is changed + - 'set_dict_actual.stdout_lines[3]|trim == "Proxy Server(s) : http=proxy:8080;https=proxy:8443;ftp=proxy:821;socks=proxy:888"' + - 'set_dict_actual.stdout_lines[4]|trim == "Bypass List : (none)"' + +- name: set proxy protocol with str + win_http_proxy: + proxy: http=proxy:8080;https=proxy:8443;ftp=proxy:821;socks=proxy:888 + register: set_str_protocol + +- name: assert set proxy protocol with str + assert: + that: + - not set_str_protocol is changed + +- name: remove proxy with empty string + win_http_proxy: + proxy: '' + register: remove_empty_str + +- name: get result of remove proxy with empty string + ansible.windows.win_command: netsh winhttp show proxy + register: remove_empty_str_actual + +- name: assert remove proxy with empty string + assert: + that: + - remove_empty_str is changed + - remove_empty_str_actual.stdout_lines[3]|trim == "Direct access (no proxy server)." + +- name: set explicit proxy for WinINet + win_inet_proxy: + proxy: proxyhost:8080 + bypass: + - abc + - def + - <local> + +- name: import proxy from IE + win_http_proxy: + source: ie + register: import_ie + +- name: get result of import proxy from IE + ansible.windows.win_command: netsh winhttp show proxy + register: import_ie_actual + +- name: assert import proxy from IE + assert: + that: + - import_ie is changed + - 'import_ie_actual.stdout_lines[3]|trim == "Proxy Server(s) : proxyhost:8080"' + - 'import_ie_actual.stdout_lines[4]|trim == "Bypass List : abc;def;<local>"' + +- name: import proxy from IE (idempotent) + win_http_proxy: + source: ie + register: import_ie_again + +- name: assert import proxy from IE (idempotent) + assert: + that: + - not import_ie_again is changed diff --git a/ansible_collections/community/windows/tests/integration/targets/win_iis_virtualdirectory/aliases b/ansible_collections/community/windows/tests/integration/targets/win_iis_virtualdirectory/aliases new file mode 100644 index 000000000..de2313a6a --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_iis_virtualdirectory/aliases @@ -0,0 +1 @@ +shippable/windows/group4
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_iis_virtualdirectory/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_iis_virtualdirectory/defaults/main.yml new file mode 100644 index 000000000..b9dfc1971 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_iis_virtualdirectory/defaults/main.yml @@ -0,0 +1,10 @@ +--- + +test_vdir_name: testvdir +test_physical_path: "{{ remote_tmp_dir }}" + +test_site_name: 'Test Site' +test_app_name: 'testapp' + +test_user: testuser +test_password: testpass
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_iis_virtualdirectory/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_iis_virtualdirectory/meta/main.yml new file mode 100644 index 000000000..e3dd5fb10 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_iis_virtualdirectory/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: +- setup_remote_tmp_dir diff --git a/ansible_collections/community/windows/tests/integration/targets/win_iis_virtualdirectory/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_iis_virtualdirectory/tasks/main.yml new file mode 100644 index 000000000..3374cbb68 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_iis_virtualdirectory/tasks/main.yml @@ -0,0 +1,90 @@ +--- +- name: check if we can run the tests + ansible.windows.win_shell: | + $osVersion = [Version](Get-Item -LiteralPath "$env:SystemRoot\System32\kernel32.dll").VersionInfo.ProductVersion + $osVersion -ge [Version]"6.2" + register: run_test + changed_when: False + +- name: Run on Server 2012 and higher + when: run_test.stdout | trim | bool + block: + - name: ensure IIS features are installed + ansible.windows.win_feature: + name: Web-Server + state: present + include_management_tools: True + register: feature_install + + - name: reboot after feature install + ansible.windows.win_reboot: + when: feature_install.reboot_required + + # may be possible that copy corrupts the file + - name: Get iis configuration checksum + ansible.windows.win_stat: + path: C:\Windows\System32\inetsrv\config\applicationHost.config + checksum_algorithm: sha1 + register: stat_result + + - name: take a copy of the original iis configuration + ansible.windows.win_copy: + src: C:\Windows\System32\inetsrv\config\applicationHost.config + dest: '{{ remote_tmp_dir }}\applicationHost.config' + remote_src: yes + register: copy_result + + - assert: + that: + - "stat_result.stat.checksum == copy_result.checksum" + + # Tests + - name: run tests on hosts that support it + include_tasks: tests.yml + + always: + # Cleanup + - name: remove test virtual directory + win_iis_virtualdirectory: + state: absent + site: "{{ test_site_name }}" + name: "{{ test_vdir_name }}" + + - name: remove test application + win_iis_webapplication: + name: "{{ test_app_name }}" + site: "{{ test_site_name }}" + state: absent + + - name: remove test site + win_iis_website: + name: "{{ test_site_name }}" + state: absent + + - name: delete test application temporary directory + win_file: + path: "{{ test_app_tmp_dir.path }}" + state: absent + + - name: restore iis configuration + ansible.windows.win_copy: + src: '{{ remote_tmp_dir }}\applicationHost.config' + dest: C:\Windows\System32\inetsrv\config\applicationHost.config + remote_src: yes + register: copy_result + + - assert: + that: + - "stat_result.stat.checksum == copy_result.checksum" + + - name: remove IIS feature if it was installed + ansible.windows.win_feature: + name: Web-Server + state: absent + include_management_tools: True + when: feature_install is changed + register: feature_uninstall + + - name: reboot after removing IIS features + ansible.windows.win_reboot: + when: feature_uninstall.reboot_required | default(False) diff --git a/ansible_collections/community/windows/tests/integration/targets/win_iis_virtualdirectory/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_iis_virtualdirectory/tasks/tests.yml new file mode 100644 index 000000000..e98271b2e --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_iis_virtualdirectory/tasks/tests.yml @@ -0,0 +1,111 @@ +--- +- name: test site exists, but stopped in case of duplicate web binding + win_iis_website: + name: "{{ test_site_name }}" + state: stopped + physical_path: 'C:\inetpub\wwwroot' + +- name: test virtual directory is absent (baseline) + win_iis_virtualdirectory: + state: absent + site: "{{ test_site_name }}" + name: "{{ test_vdir_name }}" + +- name: create test virtual directory + win_iis_virtualdirectory: + state: present + site: "{{ test_site_name }}" + name: "{{ test_vdir_name }}" + physical_path: "{{ test_physical_path }}" + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.directory.PhysicalPath == test_physical_path' + +- name: create test virtual directory (idempotent) + win_iis_virtualdirectory: + state: present + site: "{{ test_site_name }}" + name: "{{ test_vdir_name }}" + physical_path: "{{ test_physical_path }}" + register: result + +- assert: + that: + - 'result.changed == false' + - 'result.directory.PhysicalPath == test_physical_path' + +- name: set test virtual directory credentials + win_iis_virtualdirectory: + state: present + site: "{{ test_site_name }}" + name: "{{ test_vdir_name }}" + connect_as: specific_user + username: "{{ test_user }}" + password: "{{ test_password }}" + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.directory.PhysicalPath == test_physical_path' + +- name: set test virtual directory credentials (idempotent) + win_iis_virtualdirectory: + state: present + site: "{{ test_site_name }}" + name: "{{ test_vdir_name }}" + connect_as: specific_user + username: "{{ test_user }}" + password: "{{ test_password }}" + register: result + +- assert: + that: + - 'result.changed == false' + - 'result.directory.PhysicalPath == test_physical_path' + +- name: create test application temporary directory + ansible.windows.win_tempfile: + suffix: ".{{ test_app_name }}" + state: directory + register: test_app_tmp_dir + +- name: create new test application + win_iis_webapplication: + name: "{{ test_app_name }}" + site: "{{ test_site_name }}" + physical_path: "{{ test_app_tmp_dir.path }}" + state: present + +- name: create virtual directory and use pass through authentication + win_iis_virtualdirectory: + state: present + site: "{{ test_site_name }}" + name: "{{ test_vdir_name }}" + physical_path: "{{ test_physical_path }}" + connect_as: pass_through + application: "{{ test_app_name }}" + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.directory.PhysicalPath == test_physical_path' + +- name: create virtual directory and use pass through authentication (idempotent) + win_iis_virtualdirectory: + state: present + site: "{{ test_site_name }}" + name: "{{ test_vdir_name }}" + physical_path: "{{ test_physical_path }}" + connect_as: pass_through + application: "{{ test_app_name }}" + register: result + +- assert: + that: + - 'result.changed == false' + - 'result.directory.PhysicalPath == test_physical_path'
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_iis_webapplication/aliases b/ansible_collections/community/windows/tests/integration/targets/win_iis_webapplication/aliases new file mode 100644 index 000000000..54a5923a5 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_iis_webapplication/aliases @@ -0,0 +1,2 @@ +shippable/windows/group4 +unstable # Random IIS configuration errors
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_iis_webapplication/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_iis_webapplication/defaults/main.yml new file mode 100644 index 000000000..e5a582dee --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_iis_webapplication/defaults/main.yml @@ -0,0 +1,11 @@ +--- + +test_app_name: TestApp + +test_site_name: 'Test Site' + +test_user: testuser +test_password: testpass + +test_physical_path: "{{ remote_tmp_dir }}" +test_apppool: 'testapppool' diff --git a/ansible_collections/community/windows/tests/integration/targets/win_iis_webapplication/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_iis_webapplication/meta/main.yml new file mode 100644 index 000000000..e3dd5fb10 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_iis_webapplication/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: +- setup_remote_tmp_dir diff --git a/ansible_collections/community/windows/tests/integration/targets/win_iis_webapplication/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_iis_webapplication/tasks/main.yml new file mode 100644 index 000000000..64b022b6a --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_iis_webapplication/tasks/main.yml @@ -0,0 +1,84 @@ +--- +- name: check if we can run the tests + ansible.windows.win_shell: | + $osVersion = [Version](Get-Item -LiteralPath "$env:SystemRoot\System32\kernel32.dll").VersionInfo.ProductVersion + $osVersion -ge [Version]"6.2" + register: run_test + changed_when: False + +- name: Run on Server 2012 and higher + when: run_test.stdout | trim | bool + block: + - name: ensure IIS features are installed + ansible.windows.win_feature: + name: Web-Server + state: present + include_management_tools: True + register: feature_install + + - name: reboot after feature install + ansible.windows.win_reboot: + when: feature_install.reboot_required + + # may be possible that copy corrupts the file + - name: Get iis configuration checksum + ansible.windows.win_stat: + path: C:\Windows\System32\inetsrv\config\applicationHost.config + checksum_algorithm: sha1 + register: stat_result + + - name: take a copy of the original iis configuration + ansible.windows.win_copy: + src: C:\Windows\System32\inetsrv\config\applicationHost.config + dest: '{{ remote_tmp_dir }}\applicationHost.config' + remote_src: yes + register: copy_result + + - assert: + that: + - "stat_result.stat.checksum == copy_result.checksum" + + # Tests + - name: run tests on hosts that support it + include_tasks: tests.yml + + always: + # Cleanup + - name: remove test application + win_iis_webapplication: + state: absent + site: "{{ test_site_name }}" + name: "{{ test_app_name }}" + + - name: remove test application pool + win_iis_webapppool: + name: "{{ test_apppool }}" + state: absent + + - name: remove test site + win_iis_website: + name: "{{ test_site_name }}" + state: absent + + - name: restore iis configuration + ansible.windows.win_copy: + src: '{{ remote_tmp_dir }}\applicationHost.config' + dest: C:\Windows\System32\inetsrv\config\applicationHost.config + remote_src: yes + register: copy_result + + - assert: + that: + - "stat_result.stat.checksum == copy_result.checksum" + + - name: remove IIS feature if it was installed + ansible.windows.win_feature: + name: Web-Server + state: absent + include_management_tools: True + when: feature_install is changed + register: feature_uninstall + + - name: reboot after removing IIS features + ansible.windows.win_reboot: + when: feature_uninstall.reboot_required | default(False) diff --git a/ansible_collections/community/windows/tests/integration/targets/win_iis_webapplication/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_iis_webapplication/tasks/tests.yml new file mode 100644 index 000000000..135cccfec --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_iis_webapplication/tasks/tests.yml @@ -0,0 +1,91 @@ +--- +- name: test site exists, but stopped in case of duplicate web binding + win_iis_website: + name: "{{ test_site_name }}" + state: stopped + physical_path: 'C:\inetpub\wwwroot' + +- name: test app is absent (baseline) + win_iis_webapplication: + state: absent + site: "{{ test_site_name }}" + name: "{{ test_app_name }}" + +- name: create test app + win_iis_webapplication: + state: present + site: "{{ test_site_name }}" + name: "{{ test_app_name }}" + physical_path: "{{ test_physical_path }}" + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.physical_path == test_physical_path' + +- name: create test app (idempotent) + win_iis_webapplication: + state: present + site: "{{ test_site_name }}" + name: "{{ test_app_name }}" + physical_path: "{{ test_physical_path }}" + register: result + +- assert: + that: + - 'result.changed == false' + - 'result.physical_path == test_physical_path' + +- name: set test app credentials + win_iis_webapplication: + state: present + site: "{{ test_site_name }}" + name: "{{ test_app_name }}" + connect_as: specific_user + username: "{{ test_user }}" + password: "{{ test_password }}" + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.physical_path == test_physical_path' + - "result.connect_as == 'specific_user'" + +- name: set test app credentials (idempotent) + win_iis_webapplication: + state: present + site: "{{ test_site_name }}" + name: "{{ test_app_name }}" + connect_as: specific_user + username: "{{ test_user }}" + password: "{{ test_password }}" + register: result + +- assert: + that: + - 'result.changed == false' + - 'result.physical_path == test_physical_path' + - "result.connect_as == 'specific_user'" + +- name: create new test application pool + win_iis_webapppool: + name: "{{ test_apppool }}" + state: present + +- name: change app pool and use pass through authentication + win_iis_webapplication: + state: present + site: "{{ test_site_name }}" + name: "{{ test_app_name }}" + connect_as: pass_through + application_pool: "{{ test_apppool }}" + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.physical_path == test_physical_path' + - "result.connect_as == 'pass_through'" + - "result.application_pool == test_apppool" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_iis_webapppool/aliases b/ansible_collections/community/windows/tests/integration/targets/win_iis_webapppool/aliases new file mode 100644 index 000000000..3cf5b97e8 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_iis_webapppool/aliases @@ -0,0 +1 @@ +shippable/windows/group3 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_iis_webapppool/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_iis_webapppool/defaults/main.yml new file mode 100644 index 000000000..bd0f15c99 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_iis_webapppool/defaults/main.yml @@ -0,0 +1 @@ +test_iis_webapppool_name: TestPool
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_iis_webapppool/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_iis_webapppool/tasks/main.yml new file mode 100644 index 000000000..f00fbc70a --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_iis_webapppool/tasks/main.yml @@ -0,0 +1,43 @@ +--- +# Cannot use win_feature to install IIS on Server 2008. +# Run a brief check and skip hosts that don't support +# that operation +- name: check if win_feature will work on test host + ansible.windows.win_command: powershell.exe "Get-WindowsFeature" + register: module_available + failed_when: False + +# Run actual tests +- block: + - name: ensure IIS features are installed + ansible.windows.win_feature: + name: Web-Server + state: present + include_management_tools: True + register: feature_install + + - name: reboot after feature install + ansible.windows.win_reboot: + when: feature_install.reboot_required + + - name: set version of IIS for tests + win_file_version: + path: C:\Windows\System32\inetsrv\w3wp.exe + register: iis_version + + - name: ensure test pool is deleted as a baseline + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: absent + + # Tests + - name: run tests on hosts that support it + include_tasks: tests.yml + + always: + # Cleanup + - name: ensure test pool is deleted + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: absent + when: module_available.rc == 0 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_iis_webapppool/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_iis_webapppool/tasks/tests.yml new file mode 100644 index 000000000..ff28a6990 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_iis_webapppool/tasks/tests.yml @@ -0,0 +1,424 @@ +--- +- name: create default pool check + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: started + register: create_default_check + check_mode: yes + +- name: get actual of create default pool check + ansible.windows.win_command: powershell.exe "Import-Module WebAdministration; Get-Item -Path IIS:\AppPools\{{test_iis_webapppool_name}}" + register: create_default_actual_check + failed_when: False + +- name: assert create default pool check + assert: + that: + - create_default_check is changed + - create_default_actual_check.rc == 1 + +- name: create default pool + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: present + register: create_default + +- name: get actual of create default pool + ansible.windows.win_command: powershell.exe "Import-Module WebAdministration; Get-Item -Path IIS:\AppPools\{{test_iis_webapppool_name}}" + register: create_default_actual + failed_when: False + +- name: assert create default pool + assert: + that: + - create_default is changed + - create_default.info.attributes.name == test_iis_webapppool_name + - create_default.info.attributes.startMode == 'OnDemand' + - create_default.info.attributes.state == 'Started' + - create_default_actual.rc == 0 + +- name: change attributes of pool check + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: present + attributes: + managedPipelineMode: 1 # Using an enum value + cpu.limit: 95 # Nested values + processModel.identityType: LocalSystem # Using an enum name + processModel.loadUserProfile: True + register: change_pool_attributes_check + check_mode: yes + +- name: assert change attributes of pool check + assert: + that: + - change_pool_attributes_check is changed + - change_pool_attributes_check.info == create_default.info + +- name: change attributes of pool + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: present + attributes: + managedPipelineMode: 1 # Using an enum value + cpu.limit: 95 # Nested values + processModel.identityType: LocalSystem # Using an enum name + processModel.loadUserProfile: True + test: True + register: change_pool_attributes + +- name: assert change attributes of pool + assert: + that: + - change_pool_attributes is changed + - change_pool_attributes.info.attributes.managedPipelineMode == 'Classic' + - change_pool_attributes.info.cpu.limit == 95 + - change_pool_attributes.info.processModel.identityType == 'LocalSystem' + - change_pool_attributes.info.processModel.loadUserProfile == True + +- name: change attributes of pool again + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: present + attributes: + managedPipelineMode: 1 # Using an enum value + cpu.limit: 95 # Nested values + processModel.identityType: LocalSystem # Using an enum name + processModel.loadUserProfile: True + register: change_pool_attributes_again + +- name: assert change attributes of pool again + assert: + that: + - change_pool_attributes_again is not changed + - change_pool_attributes_again.info == change_pool_attributes.info + +- name: change more complex variables check + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: present + attributes: + queueLength: 500 + recycling.periodicRestart.requests: 10 # Deeply nested attribute + recycling.periodicRestart.time: "00:00:05:00.000000" # Timespan with string + processModel.pingResponseTime: "00:03:00" # Timespan without days or milliseconds + register: change_complex_attributes_check + check_mode: yes + +- name: assert change more complex variables check + assert: + that: + - change_complex_attributes_check is changed + - change_complex_attributes_check.info == change_pool_attributes_again.info + +- name: change more complex variables + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: present + attributes: + queueLength: 500 + recycling.periodicRestart.requests: 10 # Deeply nested attribute + recycling.periodicRestart.time: "00:00:05:00.000000" # Timespan with string + processModel.pingResponseTime: "00:03:00" # Timespan without days or milliseconds + register: change_complex_attributes + +- name: assert change more complex variables + assert: + that: + - change_complex_attributes is changed + - change_complex_attributes.info.attributes.queueLength == 500 + - change_complex_attributes.info.recycling.periodicRestart.requests == 10 + - change_complex_attributes.info.recycling.periodicRestart.time.TotalSeconds == 300 + - change_complex_attributes.info.processModel.pingResponseTime.TotalSeconds == 180 + +- name: change more complex variables again + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: present + attributes: + queueLength: 500 + recycling.periodicRestart.requests: 10 # Deeply nested attribute + recycling.periodicRestart.time: "00:00:05:00.000000" # Timespan with string + processModel.pingResponseTime: "00:03:00" # Timespan without days or milliseconds + register: change_complex_attributes_again + +- name: assert change more complex variables again + assert: + that: + - change_complex_attributes_again is not changed + - change_complex_attributes_again.info == change_complex_attributes.info + +- name: stop web pool check + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: stopped + register: stop_pool_check + check_mode: yes + +- name: get actual status of pool check + ansible.windows.win_command: powershell.exe "Import-Module WebAdministration; (Get-Item -Path IIS:\AppPools\{{test_iis_webapppool_name}}).state" + register: stop_pool_actual_check + +- name: assert stop web pool check + assert: + that: + - stop_pool_check is changed + - stop_pool_actual_check.stdout == 'Started\r\n' + +- name: stop web pool + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: stopped + register: stop_pool + +- name: get actual status of pool + ansible.windows.win_command: powershell.exe "Import-Module WebAdministration; (Get-Item -Path IIS:\AppPools\{{test_iis_webapppool_name}}).state" + register: stop_pool_actual + +- name: assert stop web pool + assert: + that: + - stop_pool is changed + - stop_pool_actual.stdout == 'Stopped\r\n' + +- name: stop web pool again + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: stopped + register: stop_pool_again + +- name: get actual status of pool again + ansible.windows.win_command: powershell.exe "Import-Module WebAdministration; (Get-Item -Path IIS:\AppPools\{{test_iis_webapppool_name}}).state" + register: stop_pool_actual_again + +- name: assert stop web pool again + assert: + that: + - stop_pool_again is not changed + - stop_pool_actual_again.stdout == 'Stopped\r\n' + +- name: start web pool check + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: started + register: start_pool_check + check_mode: yes + +- name: get actual status of pool check + ansible.windows.win_command: powershell.exe "Import-Module WebAdministration; (Get-Item -Path IIS:\AppPools\{{test_iis_webapppool_name}}).state" + register: start_pool_actual_check + +- name: assert start web pool check + assert: + that: + - start_pool_check is changed + - start_pool_actual_check.stdout == 'Stopped\r\n' + +- name: start web pool + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: started + register: start_pool + +- name: get actual status of pool + ansible.windows.win_command: powershell.exe "Import-Module WebAdministration; (Get-Item -Path IIS:\AppPools\{{test_iis_webapppool_name}}).state" + register: start_pool_actual + +- name: assert start web pool + assert: + that: + - start_pool is changed + - start_pool_actual.stdout == 'Started\r\n' + +- name: start web pool again + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: started + register: start_pool_again + +- name: get actual status of pool again + ansible.windows.win_command: powershell.exe "Import-Module WebAdministration; (Get-Item -Path IIS:\AppPools\{{test_iis_webapppool_name}}).state" + register: start_pool_actual_again + +- name: assert start web pool again + assert: + that: + - start_pool_again is not changed + - start_pool_actual_again.stdout == 'Started\r\n' + +- name: restart web pool + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: restarted + register: restart_pool + +- name: get actual status of pool + ansible.windows.win_command: powershell.exe "Import-Module WebAdministration; (Get-Item -Path IIS:\AppPools\{{test_iis_webapppool_name}}).state" + register: restart_pool_actual + +- name: assert restart web pool + assert: + that: + - restart_pool is changed + - restart_pool_actual.stdout == 'Started\r\n' + +- name: stop pool before restart on stop test + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: stopped + +- name: restart from stopped web pool check + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: restarted + register: restart_from_stop_pool_check + check_mode: yes + +- name: get actual status of pool check + ansible.windows.win_command: powershell.exe "Import-Module WebAdministration; (Get-Item -Path IIS:\AppPools\{{test_iis_webapppool_name}}).state" + register: restart_from_stop_pool_actual_check + +- name: assert restart from stopped web pool check + assert: + that: + - restart_from_stop_pool_check is changed + - restart_from_stop_pool_actual_check.stdout == 'Stopped\r\n' + +- name: restart from stopped web pool + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: restarted + register: restart_from_stop_pool + +- name: get actual status of pool + ansible.windows.win_command: powershell.exe "Import-Module WebAdministration; (Get-Item -Path IIS:\AppPools\{{test_iis_webapppool_name}}).state" + register: restart_from_stop_pool_actual + +- name: assert restart from stopped web pool + assert: + that: + - restart_from_stop_pool is changed + - restart_from_stop_pool_actual.stdout == 'Started\r\n' + +- name: set web pool attribute that is a collection (check mode) + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: present + attributes: + recycling.periodicRestart.schedule: "00:10:00,10:10:00" + register: collection_change_check + check_mode: yes + +- name: get result of set web pool attribute that is a collection (check mode) + ansible.windows.win_shell: | + Import-Module WebAdministration + (Get-ItemProperty -Path "IIS:\AppPools\{{test_iis_webapppool_name}}" -Name recycling.periodicRestart.schedule).Collection | ForEach-Object { $_.value.ToString() } + register: collection_change_result_check + +- name: assert results of set web pool attribute that is a collection (check mode) + assert: + that: + - collection_change_check is changed + - collection_change_result_check.stdout == "" + +- name: set web pool attribute that is a collection + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: present + attributes: + recycling.periodicRestart.schedule: "00:10:00,10:10:00" + register: collection_change + +- name: get result of set web pool attribute that is a collection + ansible.windows.win_shell: | + Import-Module WebAdministration + (Get-ItemProperty -Path "IIS:\AppPools\{{test_iis_webapppool_name}}" -Name recycling.periodicRestart.schedule).Collection | ForEach-Object { $_.value.ToString() } + register: collection_change_result + +- name: assert results of set web pool attribute that is a collection + assert: + that: + - collection_change is changed + - collection_change_result.stdout_lines == [ "00:10:00", "10:10:00" ] + +- name: set web pool attribute that is a collection (idempotent) + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: present + attributes: + recycling.periodicRestart.schedule: [ "00:10:00", "10:10:00" ] + register: collection_change_again + +- name: assert results of set web pool attribute that is a collection (idempotent) + assert: + that: + - collection_change_again is not changed + +# The following tests are only for IIS versions 8.0 or newer +- block: + - name: delete test pool + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: absent + + - name: create test pool + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: present + register: iis_attributes_blank + + - name: change attributes for newer IIS version check + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: present + attributes: + startMode: AlwaysRunning + processModel.identityType: SpecificUser + processModel.userName: '{{ansible_user}}' + processModel.password: '{{ansible_password}}' + register: iis_attributes_new_check + check_mode: yes + + - name: assert change attributes for newer IIS version check + assert: + that: + - iis_attributes_new_check is changed + - iis_attributes_new_check.info == iis_attributes_blank.info + + - name: change attributes for newer IIS version + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: present + attributes: + startMode: AlwaysRunning + processModel.identityType: SpecificUser + processModel.userName: '{{ansible_user}}' + processModel.password: '{{ansible_password}}' + register: iis_attributes_new + + - name: assert change attributes for newer IIS version + assert: + that: + - iis_attributes_new is changed + - iis_attributes_new.info.attributes.startMode == 'AlwaysRunning' + - iis_attributes_new.info.processModel.identityType == 'SpecificUser' + - iis_attributes_new.info.processModel.userName == ansible_user + + - name: change attributes for newer IIS version again + win_iis_webapppool: + name: '{{test_iis_webapppool_name}}' + state: present + attributes: + startMode: AlwaysRunning + processModel.identityType: 3 + processModel.userName: '{{ansible_user}}' + processModel.password: '{{ansible_password}}' + register: iis_attributes_new_again + + - name: assert change attributes for newer IIS version again + assert: + that: + - iis_attributes_new_again is not changed + - iis_attributes_new_again.info == iis_attributes_new.info + + when: iis_version.win_file_version.file_major_part|int > 7 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_iis_webbinding/aliases b/ansible_collections/community/windows/tests/integration/targets/win_iis_webbinding/aliases new file mode 100644 index 000000000..423ce3910 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_iis_webbinding/aliases @@ -0,0 +1 @@ +shippable/windows/group2 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_iis_webbinding/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_iis_webbinding/defaults/main.yml new file mode 100644 index 000000000..13f0bc333 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_iis_webbinding/defaults/main.yml @@ -0,0 +1,30 @@ +test_iis_site_name: default web site + +http_vars: + protocol: http + port: 80 + ip: '*' + +http_header_vars: + protocol: http + port: 80 + ip: '*' + header: test.com + +https_vars: + protocol: https + port: 443 + ip: '*' + +https_header_vars: + protocol: https + port: 443 + ip: '*' + header: test.com + ssl_flags: 1 + +https_wc_vars: + protocol: https + port: 443 + ip: '127.0.0.1' + header: wc.test.com diff --git a/ansible_collections/community/windows/tests/integration/targets/win_iis_webbinding/library/test_get_webbindings.ps1 b/ansible_collections/community/windows/tests/integration/targets/win_iis_webbinding/library/test_get_webbindings.ps1 new file mode 100644 index 000000000..f1d49f46a --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_iis_webbinding/library/test_get_webbindings.ps1 @@ -0,0 +1,106 @@ +#!powershell + +# Copyright: (c) 2017, Noah Sparks <nsparks@outlook.com> +# 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 -arguments $args -supports_check_mode $true + +$name = Get-AnsibleParam $params -name "name" -type str -failifempty $true -aliases 'website' +$host_header = Get-AnsibleParam $params -name "host_header" -type str +$protocol = Get-AnsibleParam $params -name "protocol" -type str -default 'http' +$port = Get-AnsibleParam $params -name "port" -type int -default '80' +$ip = Get-AnsibleParam $params -name "ip" -default '*' + +$result = @{ + changed = $false +} +function New-BindingInfo { + $ht = @{ + 'bindingInformation' = $args[0].bindingInformation + 'ip' = $args[0].bindingInformation.split(':')[0] + 'port' = [int]$args[0].bindingInformation.split(':')[1] + 'hostheader' = $args[0].bindingInformation.split(':')[2] + 'isDsMapperEnabled' = $args[0].isDsMapperEnabled + 'protocol' = $args[0].protocol + 'certificateStoreName' = $args[0].certificateStoreName + 'certificateHash' = $args[0].certificateHash + } + + #handle sslflag support + If ([version][System.Environment]::OSVersion.Version -lt [version]'6.2') { + $ht.sslFlags = 'not supported' + } + Else { + $ht.sslFlags = [int]$args[0].sslFlags + } + + Return $ht +} + +# Used instead of get-webbinding to ensure we always return a single binding +# pass it $binding_parameters hashtable +function Get-SingleWebBinding { + + Try { + $site_bindings = get-webbinding -name $args[0].name + } + Catch { + # 2k8r2 throws this error when you run get-webbinding with no bindings in iis + $msg = 'Cannot process argument because the value of argument "obj" is null. Change the value of argument "obj" to a non-null value' + If (-not $_.Exception.Message.CompareTo($msg)) { + Throw $_.Exception.Message + } + Else { return } + } + + Foreach ($binding in $site_bindings) { + $splits = $binding.bindingInformation -split ':' + + if ( + $args[0].protocol -eq $binding.protocol -and + $args[0].ipaddress -eq $splits[0] -and + $args[0].port -eq $splits[1] -and + $args[0].hostheader -eq $splits[2] + ) { + Return $binding + } + } +} + +# create binding search splat +$binding_parameters = @{ + Name = $name + Protocol = $protocol + Port = $port + IPAddress = $ip +} + +# insert host header to search if specified, otherwise it will return * (all bindings matching protocol/ip) +If ($host_header) { + $binding_parameters.HostHeader = $host_header +} +Else { + $binding_parameters.HostHeader = [string]::Empty +} + +# Get bindings matching parameters +Try { + $current_bindings = Get-SingleWebBinding $binding_parameters +} +Catch { + Fail-Json -obj $result -message "Failed to retrieve bindings with Get-SingleWebBinding - $($_.Exception.Message)" +} + +If ($current_bindings) { + Try { + $binding_info = New-BindingInfo $current_bindings + } + Catch { + Fail-Json -obj $result -message "Failed to create binding info - $($_.Exception.Message)" + } + + $result.binding = $binding_info +} +exit-json -obj $result
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_iis_webbinding/tasks/failures.yml b/ansible_collections/community/windows/tests/integration/targets/win_iis_webbinding/tasks/failures.yml new file mode 100644 index 000000000..92736fe1b --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_iis_webbinding/tasks/failures.yml @@ -0,0 +1,70 @@ +- name: failure check define * for host header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: present + host_header: '*' + protocol: http + ip: '*' + register: failure + failed_when: failure.msg != "To make or remove a catch-all binding, please omit the host_header parameter entirely rather than specify host_header *" + +- debug: + var: failure + verbosity: 1 + +- block: + - name: get all websites from server + raw: powershell.exe "(get-website).name" + register: existing_sites + + - name: ensure all sites are removed for clean testing + win_iis_website: + name: "{{ item }}" + state: absent + with_items: + - "{{ existing_sites.stdout_lines }}" + + - name: add testremove site + win_iis_website: + name: testremove + state: started + physical_path: c:\inetpub\wwwroot + + - name: add bindings to testremove + win_iis_webbinding: + name: testremove + ip: "{{ item.ip }}" + port: "{{ item.port }}" + with_items: + - {ip: 127.0.0.1, port: 80} + - {ip: '*', port: 80} + + - name: remove ip * binding from testremove + win_iis_webbinding: + name: testremove + state: absent + port: 80 + ip: '*' + + - name: get the remaining binding from testremove + test_get_webbindings: + name: testremove + port: 80 + ip: 127.0.0.1 + register: test_result + + - debug: + var: test_result + verbosity: 1 + + - name: assert that remove *:80 doesn't also remove 127.0.0.1:80 + assert: + that: + - test_result.binding.ip == '127.0.0.1' + - test_result.binding.port == 80 + + always: + - name: remove websites + win_iis_website: + name: testremove + state: absent diff --git a/ansible_collections/community/windows/tests/integration/targets/win_iis_webbinding/tasks/http.yml b/ansible_collections/community/windows/tests/integration/targets/win_iis_webbinding/tasks/http.yml new file mode 100644 index 000000000..34c4cc2c1 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_iis_webbinding/tasks/http.yml @@ -0,0 +1,317 @@ +#cm add +#changed true, check nothing present +- name: CM add http binding no header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: present + protocol: "{{ http_vars.protocol }}" + ip: "{{ http_vars.ip }}" + port: "{{ http_vars.port }}" + register: http_no_header + check_mode: yes + +- name: CM get binding info no header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + protocol: "{{ http_vars.protocol }}" + ip: "{{ http_vars.ip }}" + port: "{{ http_vars.port }}" + register: get_http_no_header + changed_when: false + +- name: CM add http binding with header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: present + host_header: "{{ http_header_vars.header }}" + protocol: "{{ http_header_vars.protocol }}" + ip: "{{ http_header_vars.ip }}" + port: "{{ http_header_vars.port }}" + register: http_header + check_mode: yes + +- name: CM get binding info header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + host_header: "{{ http_header_vars.header }}" + protocol: "{{ http_header_vars.protocol }}" + ip: "{{ http_header_vars.ip }}" + port: "{{ http_header_vars.port }}" + register: get_http_header + changed_when: false + +- name: CM assert changed, but not added + assert: + that: + - http_no_header is changed + - http_no_header.binding_info is none + - get_http_no_header.binding is not defined + - http_header is changed + - http_header.binding_info is none + - get_http_header.binding is not defined + +#add +#changed true, new bindings present +- name: add http binding no header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: present + protocol: "{{ http_vars.protocol }}" + ip: "{{ http_vars.ip }}" + port: "{{ http_vars.port }}" + register: http_no_header + +- name: get binding info no header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + protocol: "{{ http_vars.protocol }}" + ip: "{{ http_vars.ip }}" + port: "{{ http_vars.port }}" + register: get_http_no_header + changed_when: false + +- name: add http binding with header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: present + host_header: "{{ http_header_vars.header }}" + protocol: "{{ http_header_vars.protocol }}" + ip: "{{ http_header_vars.ip }}" + port: "{{ http_header_vars.port }}" + register: http_header + +- name: get binding info header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + host_header: "{{ http_header_vars.header }}" + protocol: "{{ http_header_vars.protocol }}" + ip: "{{ http_header_vars.ip }}" + port: "{{ http_header_vars.port }}" + register: get_http_header + changed_when: false + +- name: assert changed and added + assert: + that: + - http_no_header is changed + - http_no_header.binding_info is defined + - http_no_header.operation_type == 'added' + - http_no_header.binding_info.ip == "{{ http_vars.ip }}" + - http_no_header.binding_info.port == {{ http_vars.port }} + - http_no_header.binding_info.protocol == "{{ http_vars.protocol }}" + - http_header is changed + - http_header.binding_info is defined + - http_header.operation_type == 'added' + - http_header.binding_info.ip == "{{ http_header_vars.ip }}" + - http_header.binding_info.port == {{ http_header_vars.port }} + - http_header.binding_info.protocol == "{{ http_header_vars.protocol }}" + - http_header.binding_info.hostheader == "{{ http_header_vars.header }}" + +#add idem +#changed false +- name: idem add http binding no header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: present + protocol: "{{ http_vars.protocol }}" + ip: "{{ http_vars.ip }}" + port: "{{ http_vars.port }}" + register: http_no_header + +- name: idem add http binding with header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: present + host_header: "{{ http_header_vars.header }}" + protocol: "{{ http_header_vars.protocol }}" + ip: "{{ http_header_vars.ip }}" + port: "{{ http_header_vars.port }}" + register: http_header + +- name: idem assert not changed + assert: + that: + - http_no_header is not changed + - http_header is not changed + +#modify +#can't test modify for http, it will add a new binding instead since +#there's no way to match existing bindings against the new parameters + +#cm remove +#changed true, bindings still present +- name: cm remove http binding no header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: absent + protocol: "{{ http_vars.protocol }}" + ip: "{{ http_vars.ip }}" + port: "{{ http_vars.port }}" + register: http_no_header + check_mode: yes + +- name: get binding info no header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + protocol: "{{ http_vars.protocol }}" + ip: "{{ http_vars.ip }}" + port: "{{ http_vars.port }}" + register: get_http_no_header + changed_when: false + +- name: cm remove http binding with header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: absent + host_header: "{{ http_header_vars.header }}" + protocol: "{{ http_header_vars.protocol }}" + ip: "{{ http_header_vars.ip }}" + port: "{{ http_header_vars.port }}" + register: http_header + check_mode: yes + +- name: get binding info header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + host_header: "{{ http_header_vars.header }}" + protocol: "{{ http_header_vars.protocol }}" + ip: "{{ http_header_vars.ip }}" + port: "{{ http_header_vars.port }}" + register: get_http_header + changed_when: false + +- name: cm remove assert changed, but still present + assert: + that: + - http_no_header is changed + - http_no_header.binding_info is defined + - http_no_header.operation_type == 'removed' + - http_no_header.binding_info.ip == "{{ http_vars.ip }}" + - http_no_header.binding_info.port == {{ http_vars.port }} + - http_no_header.binding_info.protocol == "{{ http_vars.protocol }}" + - get_http_no_header.binding is defined + - get_http_no_header.binding.ip == "{{ http_vars.ip }}" + - get_http_no_header.binding.port == {{ http_vars.port }} + - get_http_no_header.binding.protocol == "{{ http_vars.protocol }}" + - http_header is changed + - http_header.binding_info is defined + - http_header.operation_type == 'removed' + - http_header.binding_info.ip == "{{ http_header_vars.ip }}" + - http_header.binding_info.port == {{ http_header_vars.port }} + - http_header.binding_info.protocol == "{{ http_header_vars.protocol }}" + - http_header.binding_info.hostheader == "{{ http_header_vars.header }}" + - get_http_header.binding is defined + - get_http_header.binding.ip == "{{ http_header_vars.ip }}" + - get_http_header.binding.port == {{ http_header_vars.port }} + - get_http_header.binding.protocol == "{{ http_header_vars.protocol }}" + - get_http_header.binding.hostheader == "{{ http_header_vars.header }}" + + +#remove +#changed true, bindings gone +- name: remove http binding no header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: absent + protocol: "{{ http_vars.protocol }}" + ip: "{{ http_vars.ip }}" + port: "{{ http_vars.port }}" + register: http_no_header + +- name: get binding info no header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + protocol: "{{ http_vars.protocol }}" + ip: "{{ http_vars.ip }}" + port: "{{ http_vars.port }}" + register: get_http_no_header + changed_when: false + +- name: remove http binding with header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: absent + host_header: "{{ http_header_vars.header }}" + protocol: "{{ http_header_vars.protocol }}" + ip: "{{ http_header_vars.ip }}" + port: "{{ http_header_vars.port }}" + register: http_header + +- name: get binding info header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + host_header: "{{ http_header_vars.header }}" + protocol: "{{ http_header_vars.protocol }}" + ip: "{{ http_header_vars.ip }}" + port: "{{ http_header_vars.port }}" + register: get_http_header + changed_when: false + +- name: remove assert changed and gone + assert: + that: + - http_no_header is changed + - http_no_header.operation_type == 'removed' + - http_no_header.binding_info is defined + - http_no_header.binding_info.ip == "{{ http_vars.ip }}" + - http_no_header.binding_info.port == {{ http_vars.port }} + - http_no_header.binding_info.protocol == "{{ http_vars.protocol }}" + - get_http_no_header.binding is not defined + - http_header is changed + - http_header.binding_info is defined + - http_header.operation_type == 'removed' + - http_header.binding_info.ip == "{{ http_header_vars.ip }}" + - http_header.binding_info.port == {{ http_header_vars.port }} + - http_header.binding_info.protocol == "{{ http_header_vars.protocol }}" + - http_header.binding_info.hostheader == "{{ http_header_vars.header }}" + - get_http_header.binding is not defined + +#remove idem +#change false, bindings gone +- name: idem remove http binding no header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: absent + protocol: "{{ http_vars.protocol }}" + ip: "{{ http_vars.ip }}" + port: "{{ http_vars.port }}" + register: http_no_header + +- name: get binding info no header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + protocol: "{{ http_vars.protocol }}" + ip: "{{ http_vars.ip }}" + port: "{{ http_vars.port }}" + register: get_http_no_header + changed_when: false + +- name: idem remove http binding with header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: absent + host_header: "{{ http_header_vars.header }}" + protocol: "{{ http_header_vars.protocol }}" + ip: "{{ http_header_vars.ip }}" + port: "{{ http_header_vars.port }}" + register: http_header + +- name: get binding info header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + host_header: "{{ http_header_vars.header }}" + protocol: "{{ http_header_vars.protocol }}" + ip: "{{ http_header_vars.ip }}" + port: "{{ http_header_vars.port }}" + register: get_http_header + changed_when: false + +- name: idem remove assert changed and gone + assert: + that: + - http_no_header is not changed + - http_no_header.binding_info is not defined + - get_http_no_header.binding is not defined + - http_header is not changed + - http_header.binding_info is not defined + - get_http_header.binding is not defined diff --git a/ansible_collections/community/windows/tests/integration/targets/win_iis_webbinding/tasks/https-ge6.2.yml b/ansible_collections/community/windows/tests/integration/targets/win_iis_webbinding/tasks/https-ge6.2.yml new file mode 100644 index 000000000..f883c673f --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_iis_webbinding/tasks/https-ge6.2.yml @@ -0,0 +1,459 @@ +############## +### CM Add ### +############## +#changed true, check nothing present +- name: CM add https binding no header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: present + protocol: "{{ https_vars.protocol }}" + ip: "{{ https_vars.ip }}" + port: "{{ https_vars.port }}" + certificate_hash: "{{ thumbprint1.stdout_lines[0] }}" + register: https_no_header + check_mode: yes + +- name: CM get binding info no header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + protocol: "{{ https_vars.protocol }}" + ip: "{{ https_vars.ip }}" + port: "{{ https_vars.port }}" + register: get_https_no_header + changed_when: false + +- name: CM add https binding with header and SNI + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: present + host_header: "{{ https_header_vars.header }}" + protocol: "{{ https_header_vars.protocol }}" + ip: "{{ https_header_vars.ip }}" + port: "{{ https_header_vars.port }}" + ssl_flags: 1 + certificate_hash: "{{ thumbprint1.stdout_lines[0] }}" + register: https_header + check_mode: yes + +- name: CM get binding info header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + host_header: "{{ https_header_vars.header }}" + protocol: "{{ https_header_vars.protocol }}" + ip: "{{ https_header_vars.ip }}" + port: "{{ https_header_vars.port }}" + register: get_https_header + changed_when: false + +- name: CM assert changed, but not added + assert: + that: + - https_no_header is changed + - https_no_header.operation_type == 'added' + - https_no_header.binding_info is none + - get_https_no_header.binding is not defined + - https_header is changed + - https_header.operation_type == 'added' + - https_header.binding_info is none + - get_https_header.binding is not defined + +########### +### Add ### +########### +#changed true, new bindings present +- name: add https binding no header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: present + protocol: "{{ https_vars.protocol }}" + ip: "{{ https_vars.ip }}" + port: "{{ https_vars.port }}" + certificate_hash: "{{ thumbprint1.stdout_lines[0] }}" + register: https_no_header + +- name: get binding info no header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + protocol: "{{ https_vars.protocol }}" + ip: "{{ https_vars.ip }}" + port: "{{ https_vars.port }}" + register: get_https_no_header + changed_when: false + +- name: add https binding with header SNI + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: present + host_header: "{{ https_header_vars.header }}" + protocol: "{{ https_header_vars.protocol }}" + ip: "{{ https_header_vars.ip }}" + port: "{{ https_header_vars.port }}" + ssl_flags: 1 + certificate_hash: "{{ thumbprint1.stdout_lines[0] }}" + register: https_header + +- name: get binding info header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + host_header: "{{ https_header_vars.header }}" + protocol: "{{ https_header_vars.protocol }}" + ip: "{{ https_header_vars.ip }}" + port: "{{ https_header_vars.port }}" + register: get_https_header + changed_when: false + +- name: assert changed and added + assert: + that: + - https_no_header is changed + - https_no_header.operation_type == 'added' + - https_no_header.binding_info is defined + - https_no_header.binding_info.protocol == "{{ https_vars.protocol }}" + - https_no_header.binding_info.ip == "{{ https_vars.ip }}" + - https_no_header.binding_info.port == {{ https_vars.port }} + - https_no_header.binding_info.hostheader == '' + - https_no_header.binding_info.certificateHash == "{{ thumbprint1.stdout_lines[0] }}" + - https_header is changed + - https_header.operation_type == 'added' + - https_header.binding_info is defined + - https_header.binding_info.hostheader == "{{ https_header_vars.header }}" + - https_header.binding_info.protocol == "{{ https_header_vars.protocol }}" + - https_header.binding_info.ip == "{{ https_header_vars.ip }}" + - https_header.binding_info.port == {{ https_header_vars.port }} + - https_header.binding_info.certificateHash == "{{ thumbprint1.stdout_lines[0] }}" + - https_header.binding_info.sslFlags == 1 + +################ +### Idem Add ### +################ +#changed false +- name: idem add https binding no header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: present + protocol: https + ip: '*' + port: 443 + certificate_hash: "{{ thumbprint1.stdout_lines[0] }}" + register: https_no_header + +- name: idem add https binding with header and SNI + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: present + host_header: test.com + protocol: https + ip: '*' + port: 443 + ssl_flags: 1 + certificate_hash: "{{ thumbprint1.stdout_lines[0] }}" + register: https_header + +- name: idem assert not changed + assert: + that: + - https_no_header is not changed + - https_header is not changed + +################# +### CM Modify ### +################# +# changed true, verify no changes occurred + +#modify sni +- name: CM modify https binding with header, change cert + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: present + host_header: "{{ https_header_vars.header }}" + protocol: "{{ https_header_vars.protocol }}" + ip: "{{ https_header_vars.ip }}" + port: "{{ https_header_vars.port }}" + ssl_flags: 1 + certificate_hash: "{{ thumbprint2.stdout_lines[0] }}" + register: https_header + check_mode: yes + +- name: get binding info header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + host_header: "{{ https_header_vars.header }}" + protocol: "{{ https_header_vars.protocol }}" + ip: "{{ https_header_vars.ip }}" + port: "{{ https_header_vars.port }}" + register: get_https_header + changed_when: false + +- name: CM assert changed but old cert + assert: + that: + - https_header is changed + - https_header.operation_type == 'updated' + - https_header.binding_info is defined + - https_header.binding_info.ip == "{{ https_header_vars.ip }}" + - https_header.binding_info.port == {{ https_header_vars.port }} + - https_header.binding_info.protocol == "{{ https_header_vars.protocol }}" + - https_header.binding_info.hostheader == "{{ https_header_vars.header }}" + - https_header.binding_info.certificateHash == "{{ thumbprint1.stdout_lines[0] }}" + - https_header.binding_info.sslFlags == 1 + - get_https_header.binding is defined + - get_https_header.binding.ip == "{{ https_header_vars.ip }}" + - get_https_header.binding.port == {{ https_header_vars.port }} + - get_https_header.binding.protocol == "{{ https_header_vars.protocol }}" + - get_https_header.binding.hostheader == "{{ https_header_vars.header }}" + - get_https_header.binding.certificateHash == "{{ thumbprint1.stdout_lines[0] }}" + - get_https_header.binding.sslFlags == 1 + +############## +### Modify ### +############## +# modify ssl flags +- name: modify https binding with header, change cert + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: present + host_header: "{{ https_header_vars.header }}" + protocol: "{{ https_header_vars.protocol }}" + ip: "{{ https_header_vars.ip }}" + port: "{{ https_header_vars.port }}" + ssl_flags: 1 + certificate_hash: "{{ thumbprint2.stdout_lines[0] }}" + register: https_header + +- name: get binding info header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + host_header: "{{ https_header_vars.header }}" + protocol: "{{ https_header_vars.protocol }}" + ip: "{{ https_header_vars.ip }}" + port: "{{ https_header_vars.port }}" + register: get_https_header + changed_when: false + +- name: modify assert changed and new cert + assert: + that: + - https_header is changed + - https_header.operation_type == 'updated' + - https_header.binding_info is defined + - https_header.binding_info.ip == "{{ https_header_vars.ip }}" + - https_header.binding_info.port == {{ https_header_vars.port }} + - https_header.binding_info.protocol == "{{ https_header_vars.protocol }}" + - https_header.binding_info.hostheader == "{{ https_header_vars.header }}" + - https_header.binding_info.certificateHash == "{{ thumbprint2.stdout_lines[0] }}" + - https_header.binding_info.sslFlags == 1 + - get_https_header.binding is defined + - get_https_header.binding.ip == "{{ https_header_vars.ip }}" + - get_https_header.binding.port == {{ https_header_vars.port }} + - get_https_header.binding.protocol == "{{ https_header_vars.protocol }}" + - get_https_header.binding.hostheader == "{{ https_header_vars.header }}" + - get_https_header.binding.certificateHash == "{{ thumbprint2.stdout_lines[0] }}" + - get_https_header.binding.sslFlags == 1 + +################### +### Idem Modify ### +################### +#changed false + +#idem modify ssl flags +- name: idem modify https binding with header, enable SNI and change cert + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: present + host_header: "{{ https_header_vars.header }}" + protocol: "{{ https_header_vars.protocol }}" + ip: "{{ https_header_vars.ip }}" + port: "{{ https_header_vars.port }}" + ssl_flags: 1 + certificate_hash: "{{ thumbprint2.stdout_lines[0] }}" + register: https_header + +- name: idem assert not changed + assert: + that: + - https_header is not changed + +################# +### CM Remove ### +################# +#changed true, bindings still present +- name: cm remove https binding no header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: absent + protocol: "{{ https_vars.protocol }}" + ip: "{{ https_vars.ip }}" + port: "{{ https_vars.port }}" + register: https_no_header + check_mode: yes + +- name: get binding info no header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + protocol: "{{ https_vars.protocol }}" + ip: "{{ https_vars.ip }}" + port: "{{ https_vars.port }}" + register: get_https_no_header + changed_when: false + +- name: cm remove https binding with header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: absent + host_header: "{{ https_header_vars.header }}" + protocol: "{{ https_header_vars.protocol }}" + ip: "{{ https_header_vars.ip }}" + port: "{{ https_header_vars.port }}" + register: https_header + check_mode: yes + +- name: get binding info header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + host_header: "{{ https_header_vars.header }}" + protocol: "{{ https_header_vars.protocol }}" + ip: "{{ https_header_vars.ip }}" + port: "{{ https_header_vars.port }}" + register: get_https_header + changed_when: false + +- name: cm remove assert changed, but still present + assert: + that: + - https_no_header is changed + - https_no_header.operation_type == 'removed' + - https_no_header.binding_info is defined + - https_no_header.binding_info.ip == "{{ https_vars.ip }}" + - https_no_header.binding_info.port == {{ https_vars.port }} + - https_no_header.binding_info.protocol == "{{ https_vars.protocol }}" + - get_https_no_header.binding is defined + - get_https_no_header.binding.ip == "{{ https_vars.ip }}" + - get_https_no_header.binding.port == {{ https_vars.port }} + - get_https_no_header.binding.protocol == "{{ https_vars.protocol }}" + - get_https_no_header.binding.certificateHash == "{{ thumbprint1.stdout_lines[0] }}" + - https_header is changed + - https_header.binding_info is defined + - https_header.operation_type == 'removed' + - https_header.binding_info.ip == "{{ https_header_vars.ip }}" + - https_header.binding_info.port == {{ https_header_vars.port }} + - https_header.binding_info.protocol == "{{ https_header_vars.protocol }}" + - https_header.binding_info.hostheader == "{{ https_header_vars.header }}" + - get_https_header.binding is defined + - get_https_header.binding.ip == "{{ https_header_vars.ip }}" + - get_https_header.binding.port == {{ https_header_vars.port }} + - get_https_header.binding.protocol == "{{ https_header_vars.protocol }}" + - get_https_header.binding.hostheader == "{{ https_header_vars.header }}" + - get_https_header.binding.certificateHash == "{{ thumbprint2.stdout_lines[0] }}" + +############## +### remove ### +############## +#changed true, bindings gone +- name: remove https binding no header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: absent + protocol: "{{ https_vars.protocol }}" + ip: "{{ https_vars.ip }}" + port: "{{ https_vars.port }}" + register: https_no_header + +- name: get binding info no header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + protocol: "{{ https_vars.protocol }}" + ip: "{{ https_vars.ip }}" + port: "{{ https_vars.port }}" + register: get_https_no_header + changed_when: false + +- name: remove https binding with header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: absent + host_header: "{{ https_header_vars.header }}" + protocol: "{{ https_header_vars.protocol }}" + ip: "{{ https_header_vars.ip }}" + port: "{{ https_header_vars.port }}" + register: https_header + +- name: get binding info header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + host_header: "{{ https_header_vars.header }}" + protocol: "{{ https_header_vars.protocol }}" + ip: "{{ https_header_vars.ip }}" + port: "{{ https_header_vars.port }}" + register: get_https_header + changed_when: false + +- name: remove assert changed and gone + assert: + that: + - https_no_header is changed + - https_no_header.binding_info is defined + - https_no_header.operation_type == 'removed' + - https_no_header.binding_info.ip == "{{ https_vars.ip }}" + - https_no_header.binding_info.port == {{ https_vars.port }} + - https_no_header.binding_info.protocol == "{{ https_vars.protocol }}" + - get_https_no_header.binding is not defined + - https_header is changed + - https_header.binding_info is defined + - https_header.operation_type == 'removed' + - https_header.binding_info.ip == "{{ https_header_vars.ip }}" + - https_header.binding_info.port == {{ https_header_vars.port }} + - https_header.binding_info.protocol == "{{ https_header_vars.protocol }}" + - https_header.binding_info.hostheader == "{{ https_header_vars.header }}" + - get_https_header.binding is not defined + +################### +### remove idem ### +################### +#change false, bindings gone +- name: idem remove https binding no header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: absent + protocol: "{{ https_vars.protocol }}" + ip: "{{ https_vars.ip }}" + port: "{{ https_vars.port }}" + register: https_no_header + +- name: get binding info no header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + protocol: "{{ https_vars.protocol }}" + ip: "{{ https_vars.ip }}" + port: "{{ https_vars.port }}" + register: get_https_no_header + changed_when: false + +- name: idem remove https binding with header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: absent + host_header: "{{ https_header_vars.header }}" + protocol: "{{ https_header_vars.protocol }}" + ip: "{{ https_header_vars.ip }}" + port: "{{ https_header_vars.port }}" + register: https_header + +- name: get binding info header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + host_header: "{{ https_header_vars.header }}" + protocol: "{{ https_header_vars.protocol }}" + ip: "{{ https_header_vars.ip }}" + port: "{{ https_header_vars.port }}" + register: get_https_header + changed_when: false + +- name: idem remove assert changed and gone + assert: + that: + - https_no_header is not changed + - https_no_header.binding_info is not defined + - get_https_no_header.binding is not defined + - https_header is not changed + - https_header.binding_info is not defined + - get_https_header.binding is not defined diff --git a/ansible_collections/community/windows/tests/integration/targets/win_iis_webbinding/tasks/https-lt6.2.yml b/ansible_collections/community/windows/tests/integration/targets/win_iis_webbinding/tasks/https-lt6.2.yml new file mode 100644 index 000000000..1950641e8 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_iis_webbinding/tasks/https-lt6.2.yml @@ -0,0 +1,423 @@ +############## +### CM Add ### +############## +#changed true, check nothing present +- name: CM add https binding no header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: present + protocol: "{{ https_vars.protocol }}" + ip: "{{ https_vars.ip }}" + port: "{{ https_vars.port }}" + certificate_hash: "{{ thumbprint1.stdout_lines[0] }}" + register: https_no_header + check_mode: yes + +- name: CM get binding info no header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + protocol: "{{ https_vars.protocol }}" + ip: "{{ https_vars.ip }}" + port: "{{ https_vars.port }}" + register: get_https_no_header + changed_when: false + +- name: CM assert changed, but not added + assert: + that: + - https_no_header is changed + - https_no_header.operation_type == 'added' + - https_no_header.binding_info is none + - get_https_no_header.binding is not defined + +########### +### Add ### +########### +#changed true, new bindings present +- name: add https binding no header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: present + protocol: "{{ https_vars.protocol }}" + ip: "{{ https_vars.ip }}" + port: "{{ https_vars.port }}" + certificate_hash: "{{ thumbprint1.stdout_lines[0] }}" + register: https_no_header + +- name: assert changed and added + assert: + that: + - https_no_header is changed + - https_no_header.binding_info is defined + - https_no_header.operation_type == 'added' + - https_no_header.binding_info.ip == "{{ https_vars.ip }}" + - https_no_header.binding_info.port == {{ https_vars.port }} + - https_no_header.binding_info.protocol == "{{ https_vars.protocol }}" + - https_no_header.binding_info.hostheader == '' + - https_no_header.binding_info.certificateHash == "{{ thumbprint1.stdout_lines[0] }}" + +################ +### Idem Add ### +################ +#changed false +- name: idem add https binding no header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: present + protocol: "{{ https_vars.protocol }}" + ip: "{{ https_vars.ip }}" + port: "{{ https_vars.port }}" + certificate_hash: "{{ thumbprint1.stdout_lines[0] }}" + register: https_no_header + +- name: idem assert not changed + assert: + that: + - https_no_header is not changed + +################# +### CM Modify ### +################# +# changed true, verify no changes occurred + +#modify sni +- name: CM modify https binding change cert + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: present + protocol: "{{ https_vars.protocol }}" + ip: "{{ https_vars.ip }}" + port: "{{ https_vars.port }}" + certificate_hash: "{{ thumbprint2.stdout_lines[0] }}" + register: https_no_header + check_mode: yes + +- name: get binding info header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + protocol: "{{ https_vars.protocol }}" + ip: "{{ https_vars.ip }}" + port: "{{ https_vars.port }}" + register: get_https_no_header + changed_when: false + +- name: CM assert changed but old cert + assert: + that: + - https_no_header is changed + - https_no_header.operation_type == 'updated' + - https_no_header.binding_info is defined + - https_no_header.binding_info.ip == "{{ https_vars.ip }}" + - https_no_header.binding_info.port == {{ https_vars.port }} + - https_no_header.binding_info.protocol == "{{ https_vars.protocol }}" + - https_no_header.binding_info.certificateHash == "{{ thumbprint1.stdout_lines[0] }}" + - get_https_no_header.binding is defined + - get_https_no_header.binding.ip == "{{ https_vars.ip }}" + - get_https_no_header.binding.port == {{ https_vars.port }} + - get_https_no_header.binding.protocol == "{{ https_vars.protocol }}" + - get_https_no_header.binding.certificateHash == "{{ thumbprint1.stdout_lines[0] }}" + +############## +### Modify ### +############## +# modify ssl flags +- name: modify https binding, change cert + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: present + protocol: "{{ https_vars.protocol }}" + ip: "{{ https_vars.ip }}" + port: "{{ https_vars.port }}" + certificate_hash: "{{ thumbprint2.stdout_lines[0] }}" + register: https_no_header + +- name: get binding info header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + protocol: "{{ https_vars.protocol }}" + ip: "{{ https_vars.ip }}" + port: "{{ https_vars.port }}" + register: get_https_no_header + changed_when: false + +- name: modify assert changed and new cert + assert: + that: + - https_no_header is changed + - https_no_header.operation_type == 'updated' + - https_no_header.binding_info is defined + - https_no_header.binding_info.ip == "{{ https_vars.ip }}" + - https_no_header.binding_info.port == {{ https_vars.port }} + - https_no_header.binding_info.protocol == "{{ https_vars.protocol }}" + - https_no_header.binding_info.certificateHash == "{{ thumbprint2.stdout_lines[0] }}" + - get_https_no_header.binding is defined + - get_https_no_header.binding.ip == "{{ https_vars.ip }}" + - get_https_no_header.binding.port == {{ https_vars.port }} + - get_https_no_header.binding.protocol == "{{ https_vars.protocol }}" + - get_https_no_header.binding.hostheader == '' + - get_https_no_header.binding.certificateHash == "{{ thumbprint2.stdout_lines[0] }}" + +################### +### Idem Modify ### +################### +#changed false + +#idem modify ssl flags +- name: idem modify https binding and change cert + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: present + protocol: "{{ https_vars.protocol }}" + ip: "{{ https_vars.ip }}" + port: "{{ https_vars.port }}" + certificate_hash: "{{ thumbprint2.stdout_lines[0] }}" + register: https_header + +- name: idem assert not changed + assert: + that: + - https_header is not changed + +################# +### CM Remove ### +################# +#changed true, bindings still present +- name: cm remove https binding no header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: absent + protocol: "{{ https_vars.protocol }}" + ip: "{{ https_vars.ip }}" + port: "{{ https_vars.port }}" + register: https_no_header + check_mode: yes + +- name: get binding info no header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + protocol: "{{ https_vars.protocol }}" + ip: "{{ https_vars.ip }}" + port: "{{ https_vars.port }}" + register: get_https_no_header + changed_when: false + +- name: cm remove assert changed, but still present + assert: + that: + - https_no_header is changed + - https_no_header.operation_type == 'removed' + - https_no_header.binding_info is defined + - https_no_header.binding_info.ip == "{{ https_vars.ip }}" + - https_no_header.binding_info.port == {{ https_vars.port }} + - https_no_header.binding_info.protocol == "{{ https_vars.protocol }}" + - https_no_header.binding_info.certificateHash == "{{ thumbprint2.stdout_lines[0] }}" + - get_https_no_header.binding is defined + - get_https_no_header.binding.ip == "{{ https_vars.ip }}" + - get_https_no_header.binding.port == {{ https_vars.port }} + - get_https_no_header.binding.protocol == "{{ https_vars.protocol }}" + - get_https_no_header.binding.certificateHash == "{{ thumbprint2.stdout_lines[0] }}" + +############## +### remove ### +############## +#changed true, bindings gone +- name: remove https binding no header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: absent + protocol: "{{ https_vars.protocol }}" + ip: "{{ https_vars.ip }}" + port: "{{ https_vars.port }}" + register: https_no_header + +- name: get binding info no header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + protocol: "{{ https_vars.protocol }}" + ip: "{{ https_vars.ip }}" + port: "{{ https_vars.port }}" + register: get_https_no_header + changed_when: false + +- name: remove assert changed and gone + assert: + that: + - https_no_header is changed + - https_no_header.operation_type == 'removed' + - https_no_header.binding_info is defined + - https_no_header.binding_info.ip == "{{ https_vars.ip }}" + - https_no_header.binding_info.port == {{ https_vars.port }} + - https_no_header.binding_info.protocol == "{{ https_vars.protocol }}" + - get_https_no_header.binding is not defined + +################### +### remove idem ### +################### +#change false, bindings gone +- name: idem remove https binding no header + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: absent + protocol: "{{ https_vars.protocol }}" + ip: "{{ https_vars.ip }}" + port: "{{ https_vars.port }}" + register: https_no_header + +- name: get binding info no header + test_get_webbindings: + name: "{{ test_iis_site_name }}" + protocol: "{{ https_vars.protocol }}" + ip: "{{ https_vars.ip }}" + port: "{{ https_vars.port }}" + register: get_https_no_header + changed_when: false + +- name: idem remove assert changed and gone + assert: + that: + - https_no_header is not changed + - https_no_header.binding_info is not defined + - get_https_no_header.binding is not defined + + +################## +### WC Testing ### +################## + +# Unfortunately this does not work due to some strange errors +# that are caused when using a self signed wildcard cert. +# I'm leaving this here in case someone finds a solution in the +# future. + +# - name: add https binding wildcard with header +# win_iis_webbinding: +# name: "{{ test_iis_site_name }}" +# state: present +# host_header: "{{ https_wc_vars.header }}" +# protocol: "{{ https_wc_vars.protocol }}" +# ip: "{{ https_wc_vars.ip }}" +# port: "{{ https_wc_vars.port }}" +# certificate_hash: "{{ thumbprint_wc.stdout_lines[0] }}" +# register: https_header + +# - name: assert changed and added +# assert: +# that: +# - https_header is changed +# - https_header.added is defined +# - https_header.added.ip == "{{ https_wc_vars.ip }}" +# - https_header.added.port == {{ https_wc_vars.port }} +# - https_header.added.protocol == "{{ https_wc_vars.protocol }}" +# - https_header.added.hostheader == "{{ https_wc_vars.header }}" +# - https_header.added.certificateHash == "{{ thumbprint_wc.stdout_lines[0] }}" + + +# - name: idem add https binding wildcard with header +# win_iis_webbinding: +# name: "{{ test_iis_site_name }}" +# state: present +# host_header: "{{ https_wc_vars.header }}" +# protocol: "{{ https_wc_vars.protocol }}" +# ip: "{{ https_wc_vars.ip }}" +# port: "{{ https_wc_vars.port }}" +# certificate_hash: "{{ thumbprint_wc.stdout_lines[0] }}" +# register: https_header + + +# - name: cm remove wildcard https binding +# win_iis_webbinding: +# name: "{{ test_iis_site_name }}" +# state: absent +# host_header: "{{ https_wc_vars.header }}" +# protocol: "{{ https_wc_vars.protocol }}" +# ip: "{{ https_wc_vars.ip }}" +# port: "{{ https_wc_vars.port }}" +# register: https_header +# check_mode: yes + +# - name: get binding info header +# test_get_webbindings: +# name: "{{ test_iis_site_name }}" +# host_header: "{{ https_wc_vars.header }}" +# protocol: "{{ https_wc_vars.protocol }}" +# ip: "{{ https_wc_vars.ip }}" +# port: "{{ https_wc_vars.port }}" +# register: get_https_header +# changed_when: false + +# - name: cm remove assert changed, but still present +# assert: +# that: +# - https_header is changed +# - https_header.removed is defined +# - https_header.removed.ip == "{{ https_wc_vars.ip }}" +# - https_header.removed.port == {{ https_wc_vars.port }} +# - https_header.removed.protocol == "{{ https_wc_vars.protocol }}" +# - https_header.removed.hostheader == "{{ https_wc_vars.header }}" +# - https_header.removed.certificateHash == "{{ thumbprint_wc.stdout_lines[0] }}" +# - get_https_header.binding is defined +# - get_https_header.removed.ip == "{{ https_wc_vars.ip }}" +# - get_https_header.removed.port == {{ https_wc_vars.port }} +# - get_https_header.removed.protocol == "{{ https_wc_vars.protocol }}" +# - get_https_header.removed.hostheader == "{{ https_wc_vars.header }}" +# - get_https_header.removed.certificateHash == "{{ thumbprint_wc.stdout_lines[0] }}" + +# - name: remove wildcard https binding +# win_iis_webbinding: +# name: "{{ test_iis_site_name }}" +# state: absent +# host_header: "{{ https_wc_vars.header }}" +# protocol: "{{ https_wc_vars.protocol }}" +# ip: "{{ https_wc_vars.ip }}" +# port: "{{ https_wc_vars.port }}" +# register: https_header + +# - name: get binding info header +# test_get_webbindings: +# name: "{{ test_iis_site_name }}" +# host_header: "{{ https_wc_vars.header }}" +# protocol: "{{ https_wc_vars.protocol }}" +# ip: "{{ https_wc_vars.ip }}" +# port: "{{ https_wc_vars.port }}" +# register: get_https_header +# changed_when: false + + +# - name: remove assert changed and gone +# assert: +# that: +# - https_header is changed +# - https_header.removed is defined +# - https_header.removed.ip == "{{ https_wc_vars.ip }}" +# - https_header.removed.port == {{ https_wc_vars.port }} +# - https_header.removed.protocol == "{{ https_wc_vars.protocol }}" +# - https_header.removed.hostheader == "{{ https_wc_vars.header }}" +# - https_header.removed.certificateHash == "{{ thumbprint_wc.stdout_lines[0] }}" +# - get_https_header.binding is not defined + +# - name: idem remove wildcard https binding +# win_iis_webbinding: +# name: "{{ test_iis_site_name }}" +# state: absent +# host_header: "{{ https_wc_vars.header }}" +# protocol: "{{ https_wc_vars.protocol }}" +# ip: "{{ https_wc_vars.ip }}" +# port: "{{ https_wc_vars.port }}" +# register: https_header + +# - name: get binding info header +# test_get_webbindings: +# name: "{{ test_iis_site_name }}" +# host_header: "{{ https_wc_vars.header }}" +# protocol: "{{ https_wc_vars.protocol }}" +# ip: "{{ https_wc_vars.ip }}" +# port: "{{ https_wc_vars.port }}" +# register: get_https_header +# changed_when: false + +# - name: idem remove assert changed and gone +# assert: +# that: +# - https_header is not changed +# - https_header.removed is not defined +# - get_https_header.binding is not defined diff --git a/ansible_collections/community/windows/tests/integration/targets/win_iis_webbinding/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_iis_webbinding/tasks/main.yml new file mode 100644 index 000000000..ee12ff78b --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_iis_webbinding/tasks/main.yml @@ -0,0 +1,62 @@ +--- +# Cannot use win_feature to install IIS on Server 2008. +# Run a brief check and skip hosts that don't support +# that operation +#seems "raw" is the only module that works on 2008 non-r2. win_command and win_shell both failed +- name: register os version (seems integration tests don't gather this fact) + raw: powershell.exe "gwmi Win32_OperatingSystem | select -expand version" + register: os_version + changed_when: False + +- block: + - include_tasks: setup.yml + - include_tasks: http.yml + - include_tasks: https-lt6.2.yml + when: os_version.stdout_lines[0] is version('6.2','lt') + - include_tasks: https-ge6.2.yml + when: os_version.stdout_lines[0] is version('6.2','ge') + - include_tasks: failures.yml + + always: + - name: get all websites from server + raw: powershell.exe "(get-website).name" + register: existing_sites + + - name: ensure all sites are removed for clean testing + win_iis_website: + name: "{{ item }}" + state: absent + with_items: + - "{{ existing_sites.stdout_lines }}" + + - name: cleanup certreq files + ansible.windows.win_file: + path: "{{ item }}" + state: absent + with_items: + - c:\windows\temp\certreq1.txt + - c:\windows\temp\certreq2.txt + - c:\windows\temp\certreqwc.txt + - c:\windows\temp\certreqresp1.txt + - c:\windows\temp\certreqresp2.txt + - c:\windows\temp\certreqrespwc.txt + + - name: remove certs + raw: 'remove-item cert:\localmachine\my\{{ item }} -force -ea silentlycontinue' + with_items: + - "{{ thumbprint1.stdout_lines[0] }}" + - "{{ thumbprint2.stdout_lines[0] }}" + - "{{ thumbprint_wc.stdout_lines[0] }}" + + - name: remove IIS features after test + ansible.windows.win_feature: + name: Web-Server + state: absent + includ_sub_features: True + include_management_tools: True + register: feature_uninstall + + - name: reboot after feature install + ansible.windows.win_reboot: + when: feature_uninstall.reboot_required + when: os_version.stdout_lines[0] is version('6.1','gt') diff --git a/ansible_collections/community/windows/tests/integration/targets/win_iis_webbinding/tasks/setup.yml b/ansible_collections/community/windows/tests/integration/targets/win_iis_webbinding/tasks/setup.yml new file mode 100644 index 000000000..234cc400d --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_iis_webbinding/tasks/setup.yml @@ -0,0 +1,93 @@ +- name: reboot before feature install to ensure server is in clean state + ansible.windows.win_reboot: + +- name: ensure IIS features are installed + ansible.windows.win_feature: + name: Web-Server + state: present + includ_sub_features: True + include_management_tools: True + register: feature_install + +- name: reboot after feature install + ansible.windows.win_reboot: + when: feature_install.reboot_required + +- name: get all websites from server + raw: powershell.exe "(get-website).name" + register: existing_sites + +- name: ensure all sites are removed for clean testing + win_iis_website: + name: "{{ item }}" + state: absent + with_items: + - "{{ existing_sites.stdout_lines }}" + +- name: add testing site {{ test_iis_site_name }} + win_iis_website: + name: "{{ test_iis_site_name }}" + physical_path: c:\inetpub\wwwroot + +- name: ensure all bindings are removed prior to starting testing + win_iis_webbinding: + name: "{{ test_iis_site_name }}" + state: absent + protocol: "{{ item.protocol }}" + port: "{{ item.port }}" + with_items: + - {protocol: http, port: 80} + - {protocol: https, port: 443} + +- name: copy certreq file + ansible.windows.win_copy: + content: |- + [NewRequest] + Subject = "CN={{ item.name }}" + KeyLength = 2048 + KeyAlgorithm = RSA + MachineKeySet = true + RequestType = Cert + dest: "{{ item.dest }}" + with_items: + - {name: test.com, dest: 'c:\windows\temp\certreq1.txt'} + - {name: test1.com, dest: 'c:\windows\temp\certreq2.txt'} + - {name: '*.test.com', dest: 'c:\windows\temp\certreqwc.txt'} + +- name: make sure response files are absent + ansible.windows.win_file: + path: "{{ item }}" + state: absent + with_items: + - 'c:\windows\temp\certreqresp1.txt' + - 'c:\windows\temp\certreqresp2.txt' + - 'c:\windows\temp\certreqrespwc.txt' + +- name: create self signed cert from certreq + ansible.windows.win_command: certreq -new -machine {{ item.req }} {{ item.resp }} + with_items: + - {req: 'c:\windows\temp\certreq1.txt', resp: 'c:\windows\temp\certreqresp1.txt'} + - {req: 'c:\windows\temp\certreq2.txt', resp: 'c:\windows\temp\certreqresp2.txt'} + - {req: 'c:\windows\temp\certreqwc.txt', resp: 'c:\windows\temp\certreqrespwc.txt'} + +- name: register certificate thumbprint1 + raw: '(gci Cert:\LocalMachine\my | ? {$_.subject -eq "CN=test.com"})[0].Thumbprint' + register: thumbprint1 + +- name: register certificate thumbprint2 + raw: '(gci Cert:\LocalMachine\my | ? {$_.subject -eq "CN=test1.com"})[0].Thumbprint' + register: thumbprint2 + +- name: register certificate thumbprint_wc + raw: '(gci Cert:\LocalMachine\my | ? {$_.subject -eq "CN=*.test.com"})[0].Thumbprint' + register: thumbprint_wc + +- debug: + var: thumbprint1.stdout + verbosity: 1 +- debug: + var: thumbprint2.stdout + verbosity: 1 +- debug: + var: thumbprint_wc.stdout + verbosity: 1 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_inet_proxy/aliases b/ansible_collections/community/windows/tests/integration/targets/win_inet_proxy/aliases new file mode 100644 index 000000000..423ce3910 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_inet_proxy/aliases @@ -0,0 +1 @@ +shippable/windows/group2 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_inet_proxy/library/win_inet_proxy_info.ps1 b/ansible_collections/community/windows/tests/integration/targets/win_inet_proxy/library/win_inet_proxy_info.ps1 new file mode 100644 index 000000000..76bf03ec9 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_inet_proxy/library/win_inet_proxy_info.ps1 @@ -0,0 +1,275 @@ +#!powershell + +# Copyright: (c) 2019, 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.AddType + +$spec = @{ + options = @{ + connection = @{ type = "str" } + } + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$connection = $module.Params.connection + +$win_inet_invoke = @' +using Microsoft.Win32.SafeHandles; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; + +namespace Ansible.WinINetProxyInfo +{ + internal class NativeHelpers + { + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public class INTERNET_PER_CONN_OPTION_LISTW : IDisposable + { + public UInt32 dwSize; + public IntPtr pszConnection; + public UInt32 dwOptionCount; + public UInt32 dwOptionError; + public IntPtr pOptions; + + public INTERNET_PER_CONN_OPTION_LISTW() + { + dwSize = (UInt32)Marshal.SizeOf(this); + } + + public void Dispose() + { + if (pszConnection != IntPtr.Zero) + Marshal.FreeHGlobal(pszConnection); + if (pOptions != IntPtr.Zero) + Marshal.FreeHGlobal(pOptions); + GC.SuppressFinalize(this); + } + ~INTERNET_PER_CONN_OPTION_LISTW() { this.Dispose(); } + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public class INTERNET_PER_CONN_OPTIONW : IDisposable + { + public INTERNET_PER_CONN_OPTION dwOption; + public ValueUnion Value; + + [StructLayout(LayoutKind.Explicit)] + public class ValueUnion + { + [FieldOffset(0)] + public UInt32 dwValue; + + [FieldOffset(0)] + public IntPtr pszValue; + + [FieldOffset(0)] + public System.Runtime.InteropServices.ComTypes.FILETIME ftValue; + } + + public void Dispose() + { + // We can't just check if Value.pszValue is not IntPtr.Zero as the union means it could be set even + // when the value is a UInt32 or FILETIME. We check against a known string option type and only free + // the value in those cases. + List<INTERNET_PER_CONN_OPTION> stringOptions = new List<INTERNET_PER_CONN_OPTION> + { + { INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_AUTOCONFIG_URL }, + { INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_BYPASS }, + { INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_SERVER } + }; + if (Value != null && Value.pszValue != IntPtr.Zero && stringOptions.Contains(dwOption)) + Marshal.FreeHGlobal(Value.pszValue); + GC.SuppressFinalize(this); + } + ~INTERNET_PER_CONN_OPTIONW() { this.Dispose(); } + } + + public enum INTERNET_OPTION : uint + { + INTERNET_OPTION_PER_CONNECTION_OPTION = 75, + } + + public enum INTERNET_PER_CONN_OPTION : uint + { + INTERNET_PER_CONN_FLAGS = 1, + INTERNET_PER_CONN_PROXY_SERVER = 2, + INTERNET_PER_CONN_PROXY_BYPASS = 3, + INTERNET_PER_CONN_AUTOCONFIG_URL = 4, + INTERNET_PER_CONN_AUTODISCOVERY_FLAGS = 5, + INTERNET_PER_CONN_FLAGS_UI = 10, // IE8+ - Included with Windows 7 and Server 2008 R2 + } + + [Flags] + public enum PER_CONN_FLAGS : uint + { + PROXY_TYPE_DIRECT = 0x00000001, + PROXY_TYPE_PROXY = 0x00000002, + PROXY_TYPE_AUTO_PROXY_URL = 0x00000004, + PROXY_TYPE_AUTO_DETECT = 0x00000008, + } + } + + internal class NativeMethods + { + [DllImport("Wininet.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool InternetQueryOptionW( + IntPtr hInternet, + NativeHelpers.INTERNET_OPTION dwOption, + SafeMemoryBuffer lpBuffer, + ref UInt32 lpdwBufferLength); + } + + internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeMemoryBuffer() : base(true) { } + public SafeMemoryBuffer(int cb) : base(true) + { + 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; + } + } + + public class WinINetProxy + { + private string Connection; + + public string AutoConfigUrl; + public bool AutoDetect; + public string Proxy; + public string ProxyBypass; + + public WinINetProxy(string connection) + { + Connection = connection; + Refresh(); + } + + public void Refresh() + { + using (var connFlags = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_FLAGS_UI)) + using (var autoConfigUrl = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_AUTOCONFIG_URL)) + using (var server = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_SERVER)) + using (var bypass = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_BYPASS)) + { + NativeHelpers.INTERNET_PER_CONN_OPTIONW[] options = new NativeHelpers.INTERNET_PER_CONN_OPTIONW[] + { + connFlags, autoConfigUrl, server, bypass + }; + + try + { + QueryOption(options, Connection); + } + catch (Win32Exception e) + { + if (e.NativeErrorCode == 87) // ERROR_INVALID_PARAMETER + { + // INTERNET_PER_CONN_FLAGS_UI only works for IE8+, try the fallback in case we are still working + // with an ancient version. + connFlags.dwOption = NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_FLAGS; + QueryOption(options, Connection); + } + else + throw; + } + + NativeHelpers.PER_CONN_FLAGS flags = (NativeHelpers.PER_CONN_FLAGS)connFlags.Value.dwValue; + + AutoConfigUrl = flags.HasFlag(NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_AUTO_PROXY_URL) + ? Marshal.PtrToStringUni(autoConfigUrl.Value.pszValue) : null; + AutoDetect = flags.HasFlag(NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_AUTO_DETECT); + if (flags.HasFlag(NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_PROXY)) + { + Proxy = Marshal.PtrToStringUni(server.Value.pszValue); + ProxyBypass = Marshal.PtrToStringUni(bypass.Value.pszValue); + } + else + { + Proxy = null; + ProxyBypass = null; + } + } + } + + internal static NativeHelpers.INTERNET_PER_CONN_OPTIONW CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION option) + { + return new NativeHelpers.INTERNET_PER_CONN_OPTIONW + { + dwOption = option, + Value = new NativeHelpers.INTERNET_PER_CONN_OPTIONW.ValueUnion(), + }; + } + + internal static void QueryOption(NativeHelpers.INTERNET_PER_CONN_OPTIONW[] options, string connection = null) + { + using (NativeHelpers.INTERNET_PER_CONN_OPTION_LISTW optionList = new NativeHelpers.INTERNET_PER_CONN_OPTION_LISTW()) + using (SafeMemoryBuffer optionListPtr = MarshalOptionList(optionList, options, connection)) + { + UInt32 bufferSize = optionList.dwSize; + if (!NativeMethods.InternetQueryOptionW( + IntPtr.Zero, + NativeHelpers.INTERNET_OPTION.INTERNET_OPTION_PER_CONNECTION_OPTION, + optionListPtr, + ref bufferSize)) + { + throw new Win32Exception(); + } + + for (int i = 0; i < options.Length; i++) + { + IntPtr opt = IntPtr.Add(optionList.pOptions, i * Marshal.SizeOf(typeof(NativeHelpers.INTERNET_PER_CONN_OPTIONW))); + NativeHelpers.INTERNET_PER_CONN_OPTIONW option = (NativeHelpers.INTERNET_PER_CONN_OPTIONW)Marshal.PtrToStructure(opt, + typeof(NativeHelpers.INTERNET_PER_CONN_OPTIONW)); + options[i].Value = option.Value; + option.Value = null; // Stops the GC from freeing the same memory twice + } + } + } + + internal static SafeMemoryBuffer MarshalOptionList(NativeHelpers.INTERNET_PER_CONN_OPTION_LISTW optionList, + NativeHelpers.INTERNET_PER_CONN_OPTIONW[] options, string connection) + { + optionList.pszConnection = Marshal.StringToHGlobalUni(connection); + optionList.dwOptionCount = (UInt32)options.Length; + + int optionSize = Marshal.SizeOf(typeof(NativeHelpers.INTERNET_PER_CONN_OPTIONW)); + optionList.pOptions = Marshal.AllocHGlobal(optionSize * options.Length); + for (int i = 0; i < options.Length; i++) + { + IntPtr option = IntPtr.Add(optionList.pOptions, i * optionSize); + Marshal.StructureToPtr(options[i], option, false); + } + + SafeMemoryBuffer optionListPtr = new SafeMemoryBuffer((int)optionList.dwSize); + Marshal.StructureToPtr(optionList, optionListPtr.DangerousGetHandle(), false); + return optionListPtr; + } + } +} +'@ +Add-CSharpType -References $win_inet_invoke -AnsibleModule $module + +$proxy = New-Object -TypeName Ansible.WinINetProxyInfo.WinINetProxy -ArgumentList @(, $connection) +$module.Result.auto_config_url = $proxy.AutoConfigUrl +$module.Result.auto_detect = $proxy.AutoDetect +$module.Result.proxy = $proxy.Proxy +$module.Result.bypass = $proxy.ProxyBypass + +$module.ExitJson() diff --git a/ansible_collections/community/windows/tests/integration/targets/win_inet_proxy/library/win_phonebook_entry.ps1 b/ansible_collections/community/windows/tests/integration/targets/win_inet_proxy/library/win_phonebook_entry.ps1 new file mode 100644 index 000000000..e449af8ea --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_inet_proxy/library/win_phonebook_entry.ps1 @@ -0,0 +1,523 @@ +#!powershell + +# Copyright: (c) 2019, 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.AddType + +# This is a very basic skeleton of a possible Windows module for managing RAS connections. It is mostly barebones +# to enable testing for win_inet_proxy but I've done a bit of extra work in the PInvoke space to possible expand +# sometime in the future. + +$spec = @{ + options = @{ + device_type = @{ + type = "str" + choices = @("atm", "framerelay", "generic", "rda", "isdn", "modem", "pad", + "parallel", "pppoe", "vpn", "serial", "sonet", "sw56", "x25") + } + device_name = @{ type = "str" } + framing_protocol = @{ type = "str"; choices = @("ppp", "ras", "slip") } + name = @{ type = "str"; required = $true } + options = @{ type = "list" } + state = @{ type = "str"; choices = @("absent", "present"); default = "present" } + type = @{ type = "str"; choices = @("broadband", "direct", "phone", "vpn") } + } + required_if = @( + , @("state", "present", @("type", "device_name", "device_type", "framing_protocol")) + ) + supports_check_mode = $false +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$device_type = $module.Params.device_type +$device_name = $module.Params.device_name +$framing_protocol = $module.Params.framing_protocol +$name = $module.Params.name +$options = $module.Params.options +$state = $module.Params.state +$type = $module.Params.type + +$module.Result.guid = [System.Guid]::Empty + +$win_ras_invoke = @' +using System; +using System.Runtime.InteropServices; + +namespace Ansible.WinPhonebookEntry +{ + public class NativeHelpers + { + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public class RASENTRYW + { + public UInt32 dwSize; + public RasEntryOptions dwfOptions; + public UInt32 dwCountryId; + public UInt32 dwCountryCode; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 11)] public string szAreaCode; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 129)] public string szLocalPhoneNumber; + public UInt32 dwAlternateOffset; + public RASIPADDR ipaddr; + public RASIPADDR ipaddrDns; + public RASIPADDR ipaddrDnsAlt; + public RASIPADDR ipaddrWins; + public RASIPADDR ipaddrWinsAlt; + public UInt32 dwFrameSize; + public RasNetProtocols dwfNetProtocols; + public RasFramingProtocol dwFramingProtocol; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string szScript; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string szAutodialDll; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string szAutodialFunc; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 17)] public string szDeviceType; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 129)] public string szDeviceName; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 33)] public string szX25PadType; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 201)] public string szX25Address; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 201)] public string szX25Facilities; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 201)] public string szX25UserData; + public UInt32 dwChannels; + public UInt32 dwReserved1; + public UInt32 dwReserved2; + public UInt32 dwSubEntries; + public RasDialMode dwDialMode; + public UInt32 dwDialExtraPercent; + public UInt32 dwDialExtraSampleSeconds; + public UInt32 dwHangUpExtraPercent; + public UInt32 dwHangUpExtraSampleSeconds; + public UInt32 dwIdleDisconnectSeconds; + public RasEntryTypes dwType; + public RasEntryEncryption dwEntryptionType; + public UInt32 dwCustomAuthKey; + public Guid guidId; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string szCustomDialDll; + public RasVpnStrategy dwVpnStrategy; + public RasEntryOptions2 dwfOptions2; + public UInt32 dwfOptions3; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string szDnsSuffix; + public UInt32 dwTcpWindowSize; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string szPrerequisitePbk; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 257)] public string szPrerequisiteEntry; + public UInt32 dwRedialCount; + public UInt32 dwRedialPause; + public RASIPV6ADDR ipv6addrDns; + public RASIPV6ADDR ipv6addrDnsAlt; + public UInt32 dwIPv4InterfaceMatrix; + public UInt32 dwIPv6InterfaceMatrix; + // Server 2008 R2 / Windows 7+ + // We cannot include these fields when running in Server 2008 as it will break the SizeOf calc of the struct +#if !LONGHORN + public RASIPV6ADDR ipv6addr; + public UInt32 dwIPv6PrefixLength; + public UInt32 dwNetworkOutageTime; +#endif + + public RASENTRYW() + { + this.dwSize = (UInt32)Marshal.SizeOf(this); + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct RASIPADDR + { + public byte a; + public byte b; + public byte c; + public byte d; + } + + [StructLayout(LayoutKind.Sequential)] + public struct RASIPV6ADDR + { + byte a; + byte b; + byte c; + byte d; + byte e; + byte f; + byte g; + byte h; + byte i; + byte j; + byte k; + byte l; + byte m; + byte n; + byte o; + byte p; + } + + public enum RasDialMode : uint + { + RASEDM_DialAll = 1, + RASEDM_DialAsNeeded = 2, + } + + public enum RasEntryEncryption : uint + { + ET_None = 0, + ET_Require = 1, + ET_RequireMax = 2, + ET_Optional = 3 + } + + [Flags] + public enum RasEntryOptions : uint + { + RASEO_UseCountryAndAreaCodes = 0x00000001, + RASEO_SpecificIpAddr = 0x00000002, + RASEO_SpecificNameServers = 0x00000004, + RASEO_IpHeaderCompression = 0x00000008, + RASEO_RemoteDefaultGateway = 0x00000010, + RASEO_DisableLcpExtensions = 0x00000020, + RASEO_TerminalBeforeDial = 0x00000040, + RASEO_TerminalAfterDial = 0x00000080, + RASEO_ModemLights = 0x00000100, + RASEO_SwCompression = 0x00000200, + RASEO_RequireEncrptedPw = 0x00000400, + RASEO_RequireMsEncrptedPw = 0x00000800, + RASEO_RequireDataEncrption = 0x00001000, + RASEO_NetworkLogon = 0x00002000, + RASEO_UseLogonCredentials = 0x00004000, + RASEO_PromoteAlternates = 0x00008000, + RASEO_SecureLocalFiles = 0x00010000, + RASEO_RequireEAP = 0x00020000, + RASEO_RequirePAP = 0x00040000, + RASEO_RequireSPAP = 0x00080000, + RASEO_Custom = 0x00100000, + RASEO_PreviewPhoneNumber = 0x00200000, + RASEO_SharedPhoneNumbers = 0x00800000, + RASEO_PreviewUserPw = 0x01000000, + RASEO_PreviewDomain = 0x02000000, + RASEO_ShowDialingProgress = 0x04000000, + RASEO_RequireCHAP = 0x08000000, + RASEO_RequireMsCHAP = 0x10000000, + RASEO_RequireMsCHAP2 = 0x20000000, + RASEO_RequireW95MSCHAP = 0x40000000, + RASEO_CustomScript = 0x80000000, + } + + [Flags] + public enum RasEntryOptions2 : uint + { + RASEO2_None = 0x00000000, + RASEO2_SecureFileAndPrint = 0x00000001, + RASEO2_SecureClientForMSNet = 0x00000002, + RASEO2_DontNegotiateMultilink = 0x00000004, + RASEO2_DontUseRasCredentials = 0x00000008, + RASEO2_UsePreSharedKey = 0x00000010, + RASEO2_Internet = 0x00000020, + RASEO2_DisableNbtOverIP = 0x00000040, + RASEO2_UseGlobalDeviceSettings = 0x00000080, + RASEO2_ReconnectIfDropped = 0x00000100, + RASEO2_SharePhoneNumbers = 0x00000200, + RASEO2_SecureRoutingCompartment = 0x00000400, + RASEO2_UseTypicalSettings = 0x00000800, + RASEO2_IPv6SpecificNameServers = 0x00001000, + RASEO2_IPv6RemoteDefaultGateway = 0x00002000, + RASEO2_RegisterIpWithDNS = 0x00004000, + RASEO2_UseDNSSuffixForRegistration = 0x00008000, + RASEO2_IPv4ExplicitMetric = 0x00010000, + RASEO2_IPv6ExplicitMetric = 0x00020000, + RASEO2_DisableIKENameEkuCheck = 0x00040000, + // Server 2008 R2 / Windows 7+ + RASEO2_DisableClassBasedStaticRoute = 0x00800000, + RASEO2_SpecificIPv6Addr = 0x01000000, + RASEO2_DisableMobility = 0x02000000, + RASEO2_RequireMachineCertificates = 0x04000000, + // Server 2012 / Windows 8+ + RASEO2_UsePreSharedKeyForIkev2Initiator = 0x00800000, + RASEO2_UsePreSharedKeyForIkev2Responder = 0x01000000, + RASEO2_CacheCredentials = 0x02000000, + // Server 2012 R2 / Windows 8.1+ + RASEO2_AutoTriggerCapable = 0x04000000, + RASEO2_IsThirdPartyProfile = 0x08000000, + RASEO2_AuthTypeIsOtp = 0x10000000, + // Server 2016 / Windows 10+ + RASEO2_IsAlwaysOn = 0x20000000, + RASEO2_IsPrivateNetwork = 0x40000000, + } + + public enum RasEntryTypes : uint + { + RASET_Phone = 1, + RASET_Vpn = 2, + RASET_Direct = 3, + RASET_Internet = 4, + RASET_Broadband = 5, + } + + public enum RasFramingProtocol : uint + { + RASFP_Ppp = 0x00000001, + RASFP_Slip = 0x00000002, + RASFP_Ras = 0x00000004 + } + + [Flags] + public enum RasNetProtocols : uint + { + RASNP_NetBEUI = 0x00000001, + RASNP_Ipx = 0x00000002, + RASNP_Ip = 0x00000004, + RASNP_Ipv6 = 0x00000008 + } + + public enum RasVpnStrategy : uint + { + VS_Default = 0, + VS_PptpOnly = 1, + VS_PptpFirst = 2, + VS_L2tpOnly = 3, + VS_L2tpFirst = 4, + VS_SstpOnly = 5, + VS_SstpFirst = 6, + VS_Ikev2Only = 7, + VS_Ikev2First = 8, + VS_GREOnly = 9, + VS_PptpSstp = 12, + VS_L2tpSstp = 13, + VS_Ikev2Sstp = 14, + } + } + + internal class NativeMethods + { + [DllImport("Rasapi32.dll", CharSet = CharSet.Unicode)] + public static extern UInt32 RasDeleteEntryW( + string lpszPhonebook, + string lpszEntry); + + [DllImport("Rasapi32.dll", CharSet = CharSet.Unicode)] + public static extern UInt32 RasGetEntryPropertiesW( + string lpszPhonebook, + string lpszEntry, + [In, Out] NativeHelpers.RASENTRYW lpRasEntry, + ref UInt32 dwEntryInfoSize, + IntPtr lpbDeviceInfo, + ref UInt32 dwDeviceInfoSize); + + [DllImport("Rasapi32.dll", CharSet = CharSet.Unicode)] + public static extern UInt32 RasSetEntryPropertiesW( + string lpszPhonebook, + string lpszEntry, + NativeHelpers.RASENTRYW lpRasEntry, + UInt32 dwEntryInfoSize, + IntPtr lpbDeviceInfo, + UInt32 dwDeviceInfoSize); + + [DllImport("Rasapi32.dll", CharSet = CharSet.Unicode)] + public static extern UInt32 RasValidateEntryNameW( + string lpszPhonebook, + string lpszEntry); + } + + public class Phonebook + { + public static void CreateEntry(string entry, NativeHelpers.RASENTRYW details) + { + UInt32 res = NativeMethods.RasSetEntryPropertiesW(null, entry, details, + details.dwSize, IntPtr.Zero, 0); + + if (res != 0) + throw new Exception(String.Format("RasSetEntryPropertiesW({0}) failed {1}", entry, res)); + } + + public static void DeleteEntry(string entry) + { + UInt32 res = NativeMethods.RasDeleteEntryW(null, entry); + if (res != 0) + throw new Exception(String.Format("RasDeleteEntryW({0}) failed {1}", entry, res)); + } + + public static NativeHelpers.RASENTRYW GetEntry(string entry) + { + NativeHelpers.RASENTRYW details = new NativeHelpers.RASENTRYW(); + UInt32 dwEntryInfoSize = details.dwSize; + UInt32 dwDeviceInfoSize = 0; + + UInt32 res = NativeMethods.RasGetEntryPropertiesW(null, entry, details, ref dwEntryInfoSize, + IntPtr.Zero, ref dwDeviceInfoSize); + + if (res != 0) + throw new Exception(String.Format("RasGetEntryPropertiesW({0}) failed {1}", entry, res)); + + return details; + } + + public static bool IsValidEntry(string entry) + { + // 183 == ENTRY_ALREADY_EXISTS + return NativeMethods.RasValidateEntryNameW(null, entry) == 183; + } + } +} +'@ + +$add_type_params = @{ + Reference = $win_ras_invoke + AnsibleModule = $module +} +# We need to set a custom compile option when running on Server 2008 due to the change in the RASENTRYW structure +$os_version = [Version](Get-Item -LiteralPath $env:SystemRoot\System32\kernel32.dll).VersionInfo.ProductVersion +if ($os_version -lt [Version]"6.1") { + $add_type_params.CompileSymbols = @("LONGHORN") +} +Add-CSharpType @add_type_params + +$exists = [Ansible.WinPhonebookEntry.Phonebook]::IsValidEntry($name) +if ($exists) { + $entry = [Ansible.WinPhonebookEntry.Phonebook]::GetEntry($name) + $module.Result.guid = $entry.guidId +} + +if ($state -eq "present") { + # Convert the input values to enum values + $expected_type = switch ($type) { + "broadband" { [Ansible.WinPhonebookEntry.NativeHelpers+RasEntryTypes]::RASET_Broadband } + "direct" { [Ansible.WinPhonebookEntry.NativeHelpers+RasEntryTypes]::RASET_Direct } + "phone" { [Ansible.WinPhonebookEntry.NativeHelpers+RasEntryTypes]::RASET_Phone } + "vpn" { [Ansible.WinPhonebookEntry.NativeHelpers+RasEntryTypes]::RASET_Vpn } + } + + $expected_framing_protocol = switch ($framing_protocol) { + "ppp" { [Ansible.WinPhonebookEntry.NativeHelpers+RasFramingProtocol]::RASFP_Ppp } + "ras" { [Ansible.WinPhonebookEntry.NativeHelpers+RasFramingProtocol]::RASFP_Ras } + "slip" { [Ansible.WinPhonebookEntry.NativeHelpers+RasFramingProtocol]::RASFP_Slip } + } + + $expected_options1 = [System.Collections.Generic.List`1[String]]@() + $expected_options2 = [System.Collections.Generic.List`1[String]]@() + $invalid_options = [System.Collections.Generic.List`1[String]]@() + foreach ($option in $options) { + # See https://msdn.microsoft.com/en-us/25c46850-4fb7-47a9-9645-139f0e869559 for more info on the options + # TODO: some of these options are set to indicate entries in RASENTRYW, we should automatically add them + # based on the input values. + switch ($option) { + # dwfOptions + "use_country_and_area_codes" { $expected_options1.Add("RASEO_UseCountryAndAreaCode") } + "specific_ip_addr" { $expected_options1.Add("RASEO_SpecificIpAddr") } + "specific_name_servers" { $expected_options1.Add("RASEO_SpecificNameServers") } + "ip_header_compression" { $expected_options1.Add("RASEO_IpHeaderCompression") } + "remote_default_gateway" { $expected_options1.Add("RASEO_RemoteDefaultGateway") } + "disable_lcp_extensions" { $expected_options1.Add("RASEO_DisableLcpExtensions") } + "terminal_before_dial" { $expected_options1.Add("RASEO_TerminalBeforeDial") } + "terminal_after_dial" { $expected_options1.Add("RASEO_TerminalAfterDial") } + "modem_lights" { $expected_options1.Add("RASEO_ModemLights") } + "sw_compression" { $expected_options1.Add("RASEO_SwCompression") } + "require_encrypted_password" { $expected_options1.Add("RASEO_RequireEncrptedPw") } + "require_ms_encrypted_password" { $expected_options1.Add("RASEO_RequireMsEncrptedPw") } + "require_data_encryption" { $expected_options1.Add("RASEO_RequireDataEncrption") } + "network_logon" { $expected_options1.Add("RASEO_NetworkLogon") } + "use_logon_credentials" { $expected_options1.Add("RASEO_UseLogonCredentials") } + "promote_alternates" { $expected_options1.Add("RASEO_PromoteAlternates") } + "secure_local_files" { $expected_options1.Add("RASEO_SecureLocalFiles") } + "require_eap" { $expected_options1.Add("RASEO_RequireEAP") } + "require_pap" { $expected_options1.Add("RASEO_RequirePAP") } + "require_spap" { $expected_options1.Add("RASEO_RequireSPAP") } + "custom" { $expected_options1.Add("RASEO_Custom") } + "preview_phone_number" { $expected_options1.Add("RASEO_PreviewPhoneNumber") } + "shared_phone_numbers" { $expected_options1.Add("RASEO_SharedPhoneNumbers") } + "preview_user_password" { $expected_options1.Add("RASEO_PreviewUserPw") } + "preview_domain" { $expected_options1.Add("RASEO_PreviewDomain") } + "show_dialing_progress" { $expected_options1.Add("RASEO_ShowDialingProgress") } + "require_chap" { $expected_options1.Add("RASEO_RequireCHAP") } + "require_ms_chap" { $expected_options1.Add("RASEO_RequireMsCHAP") } + "require_ms_chap2" { $expected_options1.Add("RASEO_RequireMsCHAP2") } + "require_w95_ms_chap" { $expected_options1.Add("RASEO_RequireW95MSCHAP") } + "custom_script" { $expected_options1.Add("RASEO_CustomScript") } + # dwfOptions2 + "secure_file_and_print" { $expected_options2.Add("RASEO2_SecureFileAndPrint") } + "secure_client_for_ms_net" { $expected_options2.Add("RASEO2_SecureClientForMSNet") } + "dont_negotiate_multilink" { $expected_options2.Add("RASEO2_DontNegotiateMultilink") } + "dont_use_ras_credential" { $expected_options2.Add("RASEO2_DontUseRasCredentials") } + "use_pre_shared_key" { $expected_options2.Add("RASEO2_UsePreSharedKey") } + "internet" { $expected_options2.Add("RASEO2_Internet") } + "disable_nbt_over_ip" { $expected_options2.Add("RASEO2_DisableNbtOverIP") } + "use_global_device_settings" { $expected_options2.Add("RASEO2_UseGlobalDeviceSettings") } + "reconnect_if_dropped" { $expected_options2.Add("RASEO2_ReconnectIfDropped") } + "share_phone_numbers" { $expected_options2.Add("RASEO2_SharePhoneNumbers") } + "secure_routing_compartment" { $expected_options2.Add("RASEO2_SecureRoutingCompartment") } + "use_typical_settings" { $expected_options2.Add("RASEO2_UseTypicalSettings") } + "ipv6_specific_name_servers" { $expected_options2.Add("RASEO2_IPv6SpecificNameServers") } + "ipv6_remote_default_gateway" { $expected_options2.Add("RASEO2_IPv6RemoteDefaultGateway") } + "register_ip_with_dns" { $expected_options2.Add("RASEO2_RegisterIpWithDNS") } + "use_dns_suffix_for_registration" { $expected_options2.Add("RASEO2_UseDNSSuffixForRegistration") } + "ipv4_explicit_metric" { $expected_options2.Add("RASEO2_IPv4ExplicitMetric") } + "ipv6_explicit_metric" { $expected_options2.Add("RASEO2_IPv6ExplicitMetric") } + "disable_ike_name_eku_check" { $expected_options2.Add("RASEO2_DisableIKENameEkuCheck") } + # TODO: Version check for the below, OS Version >= 6.1 + "disable_class_based_static_route" { $expected_options2.Add("RASEO2_DisableClassBasedStaticRoute") } + "specific_ipv6_addr" { $expected_options2.Add("RASEO2_SpecificIPv6Addr") } + "disable_mobility" { $expected_options2.Add("RASEO2_DisableMobility") } + "require_machine_certificates" { $expected_options2.Add("RASEO2_RequireMachineCertificates") } + # TODO: Version check for the below, OS Version >= 6.2 + "use_pre_shared_key_for_ikev2_initiator" { $expected_options2.Add("RASEO2_UsePreSharedKeyForIkev2Initiator") } + "use_pre_shared_key_for_ikev2_responder" { $expected_options2.Add("RASEO2_UsePreSharedKeyForIkev2Responder") } + "cache_credentials" { $expected_options2.Add("RASEO2_CacheCredentials") } + # TODO: Version check for the below, OS Version >= 6.3 + "auto_trigger_capable" { $expected_options2.Add("RASEO2_AutoTriggerCapable") } + "is_third_party_profile" { $expected_options2.Add("RASEO2_IsThirdPartyProfile") } + "auth_type_is_otp" { $expected_options2.Add("RASEO2_AuthTypeIsOtp") } + # TODO: Version check for the below, OS Version >= 10.0 + "is_always_on" { $expected_options2.Add("RASEO2_IsAlwaysOn") } + "is_private_network" { $expected_options2.Add("RASEO2_IsPrivateNetwork") } + default { $invalid_options.Add($option) } + } + } + if ($invalid_options.Count -gt 0) { + $module.FailJson("Encountered invalid options: $($invalid_options -join ", ")") + } + $expected_options1 = [Ansible.WinPhonebookEntry.NativeHelpers+RasEntryOptions]($expected_options1 -join ", ") + $expected_options2 = [Ansible.WinPhonebookEntry.NativeHelpers+RasEntryOptions2]($expected_options2 -join ", ") + + $property_map = @{ + szDeviceName = $device_name + szDeviceType = $device_type + dwFramingProtocol = $expected_framing_protocol + dwfOptions = $expected_options1 + dwfOptions2 = $expected_options2 + dwType = $expected_type + } + + if (-not $exists) { + $entry = New-Object -TypeName Ansible.WinPhonebookEntry.NativeHelpers+RASENTRYW + foreach ($kvp in $property_map.GetEnumerator()) { + $entry."$($kvp.Key)" = $kvp.Value + } + + [Ansible.WinPhonebookEntry.Phonebook]::CreateEntry($name, $entry) + $module.Result.changed = $true + + # Once created we then get the entry object again to retrieve the unique GUID ID to return + $entry = [Ansible.WinPhonebookEntry.Phonebook]::GetEntry($name) + $module.Result.guid = $entry.guidId + } + else { + $entry = [Ansible.WinPhonebookEntry.Phonebook]::GetEntry($name) + $changed = $false + foreach ($kvp in $property_map.GetEnumerator()) { + $key = $kvp.Key + $actual_value = $entry.$key + if ($actual_value -ne $kvp.Value) { + $entry.$key = $kvp.Value + $changed = $true + } + } + + if ($changed) { + [Ansible.WinPhonebookEntry.Phonebook]::CreateEntry($name, $entry) + $module.Result.changed = $true + } + } +} +else { + if ($exists) { + [Ansible.WinPhonebookEntry.Phonebook]::DeleteEntry($name) + $module.Result.changed = $true + } +} + +$module.ExitJson() diff --git a/ansible_collections/community/windows/tests/integration/targets/win_inet_proxy/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_inet_proxy/tasks/main.yml new file mode 100644 index 000000000..f92a60eab --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_inet_proxy/tasks/main.yml @@ -0,0 +1,16 @@ +--- +- name: make sure we start the tests with the defaults + win_inet_proxy: + +- block: + - name: run tests + include_tasks: tests.yml + + always: + - name: reset proxy back to defaults + win_inet_proxy: + + - name: remove phonebook entry + win_phonebook_entry: + name: Ansible Test Dialup + state: absent diff --git a/ansible_collections/community/windows/tests/integration/targets/win_inet_proxy/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_inet_proxy/tasks/tests.yml new file mode 100644 index 000000000..47ddfbe2e --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_inet_proxy/tasks/tests.yml @@ -0,0 +1,308 @@ +--- +- name: ensure we fail when proxy is not set with bypass + win_inet_proxy: + bypass: abc + register: fail_bypass + failed_when: 'fail_bypass.msg != "missing parameter(s) required by ''bypass'': proxy"' + +- name: ensure we fail if an invalid protocol is specified + win_inet_proxy: + proxy: + fail1: fail + fail2: fail + register: fail_proxy + failed_when: 'fail_proxy.msg != "Invalid keys found in proxy: fail1, fail2. Valid keys are http, https, ftp, socks."' + +- name: ensure we fail if invalid value is set + win_inet_proxy: + proxy: fake=proxy + register: fail_invalid + failed_when: fail_invalid.msg != "Unknown error when trying to set auto_config_url '', proxy 'fake=proxy', or bypass ''" + +- name: ensure we fail if an invalid connection is set + win_inet_proxy: + connection: Fake Connection + register: fail_connection + failed_when: fail_connection.msg != "The connection 'Fake Connection' does not exist." + +- name: check proxy is still set to Direct access + win_inet_proxy_info: + register: fail_invalid_actual + failed_when: fail_invalid_actual.proxy == 'fake=proxy' + +- name: disable auto detect (check) + win_inet_proxy: + auto_detect: no + register: disable_auto_detect_check + check_mode: yes + +- name: get result of disable auto detect (check) + win_inet_proxy_info: + register: disable_auto_detect_actual_check + +- name: assert disable auto detect (check) + assert: + that: + - disable_auto_detect_check is changed + - disable_auto_detect_actual_check.auto_detect + +- name: disable auto detect + win_inet_proxy: + auto_detect: no + register: disable_auto_detect + +- name: get result of disable auto detect + win_inet_proxy_info: + register: disable_auto_detect_actual + +- name: assert disable auto detect + assert: + that: + - disable_auto_detect is changed + - not disable_auto_detect_actual.auto_detect + +- name: disable auto detect (idempotent) + win_inet_proxy: + auto_detect: no + register: disable_auto_detect_again + +- name: assert disable auto detect (idempotent) + assert: + that: + - not disable_auto_detect_again is changed + +- name: set auto config url + win_inet_proxy: + auto_config_url: http://ansible.com/proxy.pac + register: set_auto_url + +- name: get result of set auto config url + win_inet_proxy_info: + register: set_auto_url_actual + +- name: assert set auto config url + assert: + that: + - set_auto_url is changed + - set_auto_url_actual.auto_detect + - set_auto_url_actual.auto_config_url == 'http://ansible.com/proxy.pac' + +- name: set auto config url (idempotent) + win_inet_proxy: + auto_config_url: http://ansible.com/proxy.pac + register: set_auto_url_again + +- name: set auto config url (idempotent) + assert: + that: + - not set_auto_url_again is changed + +- name: set a proxy using a string + win_inet_proxy: + proxy: proxyhost + register: proxy_str + +- name: get result of set a proxy using a string + win_inet_proxy_info: + register: proxy_str_actual + +- name: assert set a proxy using a string + assert: + that: + - proxy_str is changed + - proxy_str_actual.auto_detect + - proxy_str_actual.auto_config_url == None + - proxy_str_actual.proxy == 'proxyhost' + +- name: set a proxy using a string (idempotent) + win_inet_proxy: + proxy: proxyhost + register: proxy_str_again + +- name: assert set a proxy using a string (idempotent) + assert: + that: + - not proxy_str_again is changed + +- name: change a proxy and set bypass + win_inet_proxy: + proxy: proxyhost:8080 + bypass: + - abc + - def + - <local> + register: change_proxy + +- name: get result of change a proxy and set bypass + win_inet_proxy_info: + register: change_proxy_actual + +- name: assert change a proxy and set bypass + assert: + that: + - change_proxy is changed + - change_proxy_actual.proxy == 'proxyhost:8080' + - change_proxy_actual.bypass == 'abc;def;<local>' + +- name: change a proxy and set bypass (idempotent) + win_inet_proxy: + proxy: proxyhost:8080 + bypass: abc,def,<local> + register: change_proxy_again + +- name: assert change a proxy and set bypass (idempotent) + assert: + that: + - not change_proxy_again is changed + +- name: change bypass list + win_inet_proxy: + proxy: proxyhost:8080 + bypass: + - abc + - <-loopback> + register: change_bypass + +- name: get reuslt of change bypass list + win_inet_proxy_info: + register: change_bypass_actual + +- name: assert change bypass list + assert: + that: + - change_bypass is changed + - change_bypass_actual.proxy == 'proxyhost:8080' + - change_bypass_actual.bypass == 'abc;<-loopback>' + +- name: remove proxy without options + win_inet_proxy: + register: remove_proxy + +- name: get result of remove proxy without options + win_inet_proxy_info: + register: remove_proxy_actual + +- name: assert remove proxy without options + assert: + that: + - remove_proxy is changed + - remove_proxy_actual.auto_detect == True + - remove_proxy_actual.auto_config_url == None + - remove_proxy_actual.proxy == None + - remove_proxy_actual.bypass == None + +- name: remove proxy without options (idempotent) + win_inet_proxy: + register: remove_proxy_again + +- name: assert remove proxy without options (idempotent) + assert: + that: + - not remove_proxy_again is changed + +- name: set proxy with dictionary + win_inet_proxy: + proxy: + http: proxy:8080 + https: proxy:8443 + ftp: proxy:821 + socks: proxy:888 + register: set_dict + +- name: get result of set proxy with dictionary + win_inet_proxy_info: + register: set_dict_actual + +- name: assert set proxy with dictionary + assert: + that: + - set_dict is changed + - set_dict_actual.proxy == 'http=proxy:8080;https=proxy:8443;ftp=proxy:821;socks=proxy:888' + +- name: set proxy protocol with str + win_inet_proxy: + proxy: http=proxy:8080;https=proxy:8443;ftp=proxy:821;socks=proxy:888 + register: set_str_protocol + +- name: assert set proxy protocol with str + assert: + that: + - not set_str_protocol is changed + +- name: remove proxy with empty string + win_inet_proxy: + proxy: '' + register: remove_empty_str + +- name: get result of remove proxy with empty string + win_inet_proxy_info: + register: remove_empty_str_actual + +- name: assert remove proxy with empty string + assert: + that: + - remove_empty_str is changed + - remove_empty_str_actual.proxy == None + +- name: create test phonebook entry + win_phonebook_entry: + name: Ansible Test Dialup + device_type: pppoe + device_name: WAN Miniport (PPPOE) + framing_protocol: ppp + options: + - remote_default_gateway + - require_pap + - internet + type: broadband + state: present + +- name: set proxy for specific connection + win_inet_proxy: + connection: Ansible Test Dialup + auto_detect: no + auto_config_url: proxy.com + proxy: proxyhost:8080 + bypass: proxyhost + register: set_connection + +- name: get result for set proxy for specific connection + win_inet_proxy_info: + connection: Ansible Test Dialup + register: set_connection_actual + +- name: get result for LAN connection proxy + win_inet_proxy_info: + register: set_connection_lan_actual + +- name: assert set proxy for specific connection + assert: + that: + - set_connection is changed + - set_connection_actual.auto_detect == False + - set_connection_actual.auto_config_url == 'proxy.com' + - set_connection_actual.proxy == 'proxyhost:8080' + - set_connection_actual.bypass == 'proxyhost' + - set_connection_lan_actual.auto_detect == True + - set_connection_lan_actual.auto_config_url == None + - set_connection_lan_actual.proxy == None + - set_connection_lan_actual.bypass == None + +- name: remove proxy for specific connection + win_inet_proxy: + connection: Ansible Test Dialup + register: remove_connection + +- name: get result of remove proxy for specific connection + win_inet_proxy_info: + connection: Ansible Test Dialup + register: remove_connection_actual + +- name: assert remove proxy for specific connection + assert: + that: + - remove_connection is changed + - remove_connection_actual.auto_detect == True + - remove_connection_actual.auto_config_url == None + - remove_connection_actual.proxy == None + - remove_connection_actual.bypass == None diff --git a/ansible_collections/community/windows/tests/integration/targets/win_initialize_disk/aliases b/ansible_collections/community/windows/tests/integration/targets/win_initialize_disk/aliases new file mode 100644 index 000000000..4f4664b68 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_initialize_disk/aliases @@ -0,0 +1 @@ +shippable/windows/group5 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_initialize_disk/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_initialize_disk/defaults/main.yml new file mode 100644 index 000000000..9cb54a30c --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_initialize_disk/defaults/main.yml @@ -0,0 +1 @@ +AnsibleVhdx: C:\win_initialize_disk_tests\AnsiblePart.vhdx diff --git a/ansible_collections/community/windows/tests/integration/targets/win_initialize_disk/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_initialize_disk/tasks/main.yml new file mode 100644 index 000000000..b752a7913 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_initialize_disk/tasks/main.yml @@ -0,0 +1,28 @@ +--- +- name: Create the temp directory + ansible.windows.win_file: + path: C:\win_initialize_disk_tests + state: directory + +- name: Copy VHDX scripts + ansible.windows.win_template: + src: "{{ item.src }}" + dest: C:\win_initialize_disk_tests\{{ item.dest }} + loop: + - { src: vhdx_creation_script.j2, dest: vhdx_creation_script.txt } + - { src: vhdx_deletion_script.j2, dest: vhdx_deletion_script.txt } + +- name: Create VHD + ansible.windows.win_command: diskpart.exe /s C:\win_initialize_disk_tests\vhdx_creation_script.txt + +- name: Run tests + block: + - include: tests.yml + always: + - name: Detach disk + ansible.windows.win_command: diskpart.exe /s C:\win_initialize_disk_tests\vhdx_deletion_script.txt + + - name: Cleanup files + ansible.windows.win_file: + path: C:\win_initialize_disk_tests + state: absent diff --git a/ansible_collections/community/windows/tests/integration/targets/win_initialize_disk/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_initialize_disk/tasks/tests.yml new file mode 100644 index 000000000..b9bc1d70b --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_initialize_disk/tasks/tests.yml @@ -0,0 +1,104 @@ +--- +- name: Initialize the disk with the default partition style (check mode) + win_initialize_disk: + disk_number: 1 + register: default_part_style_check + check_mode: yes + +- name: Get result of default initialization (check mode) + ansible.windows.win_command: powershell.exe "if ( (Get-Disk -Number 1).PartitionStyle -eq 'RAW' ) {'true'} else {'false'}" + register: default_part_style_actual_check + +- name: assert default initialization (check mode) + assert: + that: + - default_part_style_check is changed + - default_part_style_actual_check.stdout == 'true\r\n' + +- name: Initialize the disk with the default partition style + win_initialize_disk: + disk_number: 1 + register: default_part_style + +- name: Get result of default initialization + ansible.windows.win_command: powershell.exe "if ( (Get-Disk -Number 1).PartitionStyle -eq 'GPT' ) {'true'} else {'false'}" + register: default_part_style_actual + +- name: assert default initialization + assert: + that: + - default_part_style is changed + - default_part_style_actual.stdout == 'true\r\n' + +- name: Initialize the disk with the default partition style (idempotence) + win_initialize_disk: + disk_number: 1 + register: default_part_style_idempotence + +- name: Get result of default initialization (idempotence) + ansible.windows.win_command: powershell.exe "if ( (Get-Disk -Number 1).PartitionStyle -eq 'GPT' ) {'true'} else {'false'}" + register: default_part_style_actual_idempotence + +- name: assert default initialization (idempotence) + assert: + that: + - not default_part_style_idempotence is changed + - default_part_style_actual_idempotence.stdout == 'true\r\n' + +- name: Partition style change without force fails + win_initialize_disk: + disk_number: 1 + style: mbr + register: change_part_style + ignore_errors: True + +- name: assert failed partition style change + assert: + that: + - change_part_style is failed + +- name: Partition style change with force is successful (check mode) + win_initialize_disk: + disk_number: 1 + style: mbr + force: yes + register: change_part_style_forced_check + check_mode: yes + +- name: Get result of forced initialization (check mode) + ansible.windows.win_command: powershell.exe "if ( (Get-Disk -Number 1).PartitionStyle -eq 'GPT' ) {'true'} else {'false'}" + register: change_part_style_forced_actual_check + +- name: assert forced initialization (check mode) + assert: + that: + - change_part_style_forced_check is changed + - change_part_style_forced_actual_check.stdout == 'true\r\n' + +- name: Partition style change with force is successful + win_initialize_disk: + disk_number: 1 + style: mbr + force: yes + register: change_part_style_forced + +- name: Get result of forced initialization + ansible.windows.win_command: powershell.exe "if ( (Get-Disk -Number 1).PartitionStyle -eq 'MBR' ) {'true'} else {'false'}" + register: change_part_style_forced_actual + +- name: assert forced initialization + assert: + that: + - change_part_style_forced is changed + - change_part_style_forced_actual.stdout == 'true\r\n' + +- name: Unknown disk number fails + win_initialize_disk: + disk_number: 3 + register: unknown_disk_number + ignore_errors: True + +- name: assert unknown disk number fails + assert: + that: + - unknown_disk_number is failed diff --git a/ansible_collections/community/windows/tests/integration/targets/win_initialize_disk/templates/vhdx_creation_script.j2 b/ansible_collections/community/windows/tests/integration/targets/win_initialize_disk/templates/vhdx_creation_script.j2 new file mode 100644 index 000000000..4089bf379 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_initialize_disk/templates/vhdx_creation_script.j2 @@ -0,0 +1,5 @@ +create vdisk file="{{ AnsibleVhdx }}" maximum=2000 type=fixed + +select vdisk file="{{ AnsibleVhdx }}" + +attach vdisk diff --git a/ansible_collections/community/windows/tests/integration/targets/win_initialize_disk/templates/vhdx_deletion_script.j2 b/ansible_collections/community/windows/tests/integration/targets/win_initialize_disk/templates/vhdx_deletion_script.j2 new file mode 100644 index 000000000..c2be9cd14 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_initialize_disk/templates/vhdx_deletion_script.j2 @@ -0,0 +1,3 @@ +select vdisk file="{{ AnsibleVhdx }}" + +detach vdisk diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/aliases b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/aliases new file mode 100644 index 000000000..4cd27b3cb --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/aliases @@ -0,0 +1 @@ +shippable/windows/group1 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/.gitattributes b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/.gitattributes new file mode 100644 index 000000000..d3f27225a --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/.gitattributes @@ -0,0 +1,4 @@ +*.text text eol=LF +*.txt text eol=CRLF +*.txt16 text working-tree-encoding=UTF-16 eol=CRLF +*.txt32 text working-tree-encoding=UTF-32 eol=CRLF diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/01_new_line_at_bof.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/01_new_line_at_bof.txt new file mode 100644 index 000000000..01a263dbc --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/01_new_line_at_bof.txt @@ -0,0 +1,6 @@ +New line at the beginning +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/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/02_new_line_at_eof.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/02_new_line_at_eof.txt new file mode 100644 index 000000000..ff0ffa89d --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/02_new_line_at_eof.txt @@ -0,0 +1,7 @@ +New line at the beginning +This is line 1 +This is line 2 +REF this is a line for backrefs REF +This is line 4 +This is line 5 +New line at the end diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/03_new_line_after_1.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/03_new_line_after_1.txt new file mode 100644 index 000000000..c13010c68 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/03_new_line_after_1.txt @@ -0,0 +1,8 @@ +New line at the beginning +This is line 1 +New line after line 1 +This is line 2 +REF this is a line for backrefs REF +This is line 4 +This is line 5 +New line at the end diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/04_new_line_before_5.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/04_new_line_before_5.txt new file mode 100644 index 000000000..9e021050b --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/04_new_line_before_5.txt @@ -0,0 +1,9 @@ +New line at the beginning +This is line 1 +New line after line 1 +This is line 2 +REF this is a line for backrefs REF +This is line 4 +New line before line 5 +This is line 5 +New line at the end diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/05_new_line_at_REF.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/05_new_line_at_REF.txt new file mode 100644 index 000000000..4a5379ae0 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/05_new_line_at_REF.txt @@ -0,0 +1,9 @@ +New line at the beginning +This is line 1 +New line after line 1 +This is line 2 +This is line 3 +This is line 4 +New line before line 5 +This is line 5 +New line at the end diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/06_remove_middle_line.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/06_remove_middle_line.txt new file mode 100644 index 000000000..bd0e8f595 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/06_remove_middle_line.txt @@ -0,0 +1,8 @@ +New line at the beginning +This is line 1 +New line after line 1 +This is line 2 +This is line 4 +New line before line 5 +This is line 5 +New line at the end diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/07_remove_line_5.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/07_remove_line_5.txt new file mode 100644 index 000000000..ae0b0ab98 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/07_remove_line_5.txt @@ -0,0 +1,7 @@ +New line at the beginning +This is line 1 +New line after line 1 +This is line 2 +This is line 4 +New line before line 5 +New line at the end diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/08_no_expected_change.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/08_no_expected_change.txt new file mode 100644 index 000000000..ae0b0ab98 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/08_no_expected_change.txt @@ -0,0 +1,7 @@ +New line at the beginning +This is line 1 +New line after line 1 +This is line 2 +This is line 4 +New line before line 5 +New line at the end diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/09_new_file.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/09_new_file.txt new file mode 100644 index 000000000..6dfa057f0 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/09_new_file.txt @@ -0,0 +1 @@ +This is a new file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/10_no_eof_new_at_eof.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/10_no_eof_new_at_eof.txt new file mode 100644 index 000000000..31b06636a --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/10_no_eof_new_at_eof.txt @@ -0,0 +1,3 @@ +This is line 1 +This is line 2 +New line at the end
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/11_multiline_at_eof.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/11_multiline_at_eof.txt new file mode 100644 index 000000000..fa15cdba3 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/11_multiline_at_eof.txt @@ -0,0 +1,9 @@ +New line at the beginning +This is line 1 +New line after line 1 +This is line 2 +This is line 4 +New line before line 5 +New line at the end +This is a line +with newline character diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/12_empty_file_add_at_eof.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/12_empty_file_add_at_eof.txt new file mode 100644 index 000000000..1467ce9ea --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/12_empty_file_add_at_eof.txt @@ -0,0 +1 @@ +New line at the end
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/13_new_4_with_backref.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/13_new_4_with_backref.txt new file mode 100644 index 000000000..3c86758a0 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/13_new_4_with_backref.txt @@ -0,0 +1,9 @@ +New line at the beginning +This is line 1 +New line after line 1 +This is line 2 +New line 4 created with the backref +New line before line 5 +New line at the end +This is a line +with newline character diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/14_quoting_code.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/14_quoting_code.txt new file mode 100644 index 000000000..3575ba049 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/14_quoting_code.txt @@ -0,0 +1,3 @@ +var dotenv = require('dotenv'); +dotenv.load(); +'foo'
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/15_single_quote.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/15_single_quote.txt new file mode 100644 index 000000000..e1e1cf090 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/15_single_quote.txt @@ -0,0 +1,4 @@ +var dotenv = require('dotenv'); +dotenv.load(); +'foo' +import g'
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/16_multiple_quotes.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/16_multiple_quotes.txt new file mode 100644 index 000000000..55a32a6f2 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/16_multiple_quotes.txt @@ -0,0 +1,5 @@ +var dotenv = require('dotenv'); +dotenv.load(); +'foo' +import g' +"quote" and "unquote"
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/17_new_file_win.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/17_new_file_win.txt new file mode 100644 index 000000000..6dfa057f0 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/17_new_file_win.txt @@ -0,0 +1 @@ +This is a new file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/18_sep_win.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/18_sep_win.txt new file mode 100644 index 000000000..b2f86910d --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/18_sep_win.txt @@ -0,0 +1,2 @@ +This is a new file +This is the last line diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/19_new_file_unix.text b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/19_new_file_unix.text new file mode 100644 index 000000000..6dfa057f0 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/19_new_file_unix.text @@ -0,0 +1 @@ +This is a new file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/20_sep_unix.text b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/20_sep_unix.text new file mode 100644 index 000000000..b2f86910d --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/20_sep_unix.text @@ -0,0 +1,2 @@ +This is a new file +This is the last line diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/21_utf8_no_bom.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/21_utf8_no_bom.txt new file mode 100644 index 000000000..cd753877d --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/21_utf8_no_bom.txt @@ -0,0 +1 @@ +This is a new utf-8 file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/22_utf8_no_bom_line_added.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/22_utf8_no_bom_line_added.txt new file mode 100644 index 000000000..3c3116a3e --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/22_utf8_no_bom_line_added.txt @@ -0,0 +1,2 @@ +This is a new utf-8 file +This is the last line diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/23_utf8_bom.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/23_utf8_bom.txt new file mode 100644 index 000000000..0873b7649 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/23_utf8_bom.txt @@ -0,0 +1 @@ +This is a new utf-8 file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/24_utf8_bom_line_added.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/24_utf8_bom_line_added.txt new file mode 100644 index 000000000..484d75488 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/24_utf8_bom_line_added.txt @@ -0,0 +1,2 @@ +This is a new utf-8 file +This is the last line diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/25_utf16.txt16 b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/25_utf16.txt16 new file mode 100644 index 000000000..b9a0d1ae2 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/25_utf16.txt16 @@ -0,0 +1 @@ +This is a new utf-16 file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/26_utf16_line_added.txt16 b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/26_utf16_line_added.txt16 new file mode 100644 index 000000000..0a949dc4e --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/26_utf16_line_added.txt16 @@ -0,0 +1,2 @@ +This is a new utf-16 file +This is the last line diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/27_utf32.txt32 b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/27_utf32.txt32 new file mode 100644 index 000000000..62e697f88 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/27_utf32.txt32 @@ -0,0 +1 @@ +This is a new utf-32 file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/28_utf32_line_added.txt32 b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/28_utf32_line_added.txt32 new file mode 100644 index 000000000..3d9af1c40 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/28_utf32_line_added.txt32 @@ -0,0 +1,2 @@ +This is a new utf-32 file +This is the last line diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/29_no_linebreak.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/29_no_linebreak.txt new file mode 100644 index 000000000..dff2fa088 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/29_no_linebreak.txt @@ -0,0 +1 @@ +c:\return\new
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/30_linebreaks_checksum_bad.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/30_linebreaks_checksum_bad.txt new file mode 100644 index 000000000..ffaa8767f --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/30_linebreaks_checksum_bad.txt @@ -0,0 +1,3 @@ +c:\return\new +c:
eturn +ew
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/31_relative_path.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/31_relative_path.txt new file mode 100644 index 000000000..01a263dbc --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/31_relative_path.txt @@ -0,0 +1,6 @@ +New line at the beginning +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/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/99_README.md b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/99_README.md new file mode 100644 index 000000000..798b9c02e --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/expectations/99_README.md @@ -0,0 +1,36 @@ +***WIN LINEINFILE Expectations*** + +This folder contains expected files as the tests in this playbook executes on the +files in 'files'. + +To get the checksum as would win_stat in the tests, go to this folder in powershell and +execute + +```powershell +Get-ChildItem | ForEach-Object { + $fp = [System.IO.File]::Open("$pwd/$($_.Name)", [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite) + Write-Output $_.Name + try { + [System.BitConverter]::ToString($sp.ComputeHash($fp)).Replace("-", "").ToLower() + } finally { + $fp.Dispose() + } + Write-Output "" +} +``` + +There is one exception right now: 30_linebreaks_checksum_bad.txt which requires mixed line endings that +git cannot handle without turning the file binary. The file should read + +``` +c:\return\newCRLF +c:CR +eturnLF +ew +``` +where CR and LF denote carriage return (\r) and line feed (\n) respectively, to get the correct checksum. + +Also, the .gitattributes files is important as it assures that the EOL characters +for the files are correct, regardless of environment. The files may be checked out on +linux but the resulting files will be created using windows EOL, and the comparison must +match.
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/test.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/test.txt new file mode 100644 index 000000000..8187db9f0 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/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/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/test_linebreak.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/test_linebreak.txt new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/test_linebreak.txt diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/test_quoting.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/test_quoting.txt new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/test_quoting.txt diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/testempty.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/testempty.txt new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/testempty.txt diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/testnoeof.txt b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/files/testnoeof.txt new file mode 100644 index 000000000..152780b9f --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/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/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/meta/main.yml new file mode 100644 index 000000000..9f37e96cd --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_remote_tmp_dir diff --git a/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/tasks/main.yml new file mode 100644 index 000000000..87748e595 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_lineinfile/tasks/main.yml @@ -0,0 +1,803 @@ +# Test code for the win_lineinfile module, adapted from the standard lineinfile module tests +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +- name: deploy the test file for lineinfile + ansible.windows.win_copy: src=test.txt dest={{ remote_tmp_dir }}/test.txt + register: result + +- name: assert that the test file was deployed + assert: + that: + - "result.changed == true" + +- name: stat the test file + # Note: The test.txt is copied from a Linux host, therefore it will retain + # unix line ending when stat'd here. This affects checksum, meaning that + # when this repo is checked out to Windows, checksum does not match checked- + # out file checksum unless line-endings are altered. + ansible.windows.win_stat: path={{ remote_tmp_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 + # 01 + win_lineinfile: dest={{ remote_tmp_dir }}/test.txt state=present line="New line at the beginning" insertbefore="BOF" backup=yes + register: result + +- name: check backup_file + ansible.windows.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 + ansible.windows.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 + ansible.windows.win_stat: path={{ remote_tmp_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 == 'c61233b0ee2038aab41b5f30683c57b2a013b376' + +- name: insert a line at the end of the file + # 02 + win_lineinfile: dest={{ remote_tmp_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 + ansible.windows.win_stat: path={{ remote_tmp_dir }}/test.txt + register: result + +- name: assert test checksum matches after the insert at the end + assert: + that: + - result.stat.checksum == 'a91260ad67b00609cc0737ff70ac5170c6a519a8' + +- name: insert a line after the first line + # 03 + win_lineinfile: dest={{ remote_tmp_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 + ansible.windows.win_stat: path={{ remote_tmp_dir }}/test.txt + register: result + +- name: assert test checksum matches after the insert after the first line + assert: + that: + - result.stat.checksum == '6ffbabdaa21ecb3593d32144de535598cfd7c6ea' + +- name: insert a line before the last line + # 04 + win_lineinfile: dest={{ remote_tmp_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 + ansible.windows.win_stat: path={{ remote_tmp_dir }}/test.txt + register: result + +- name: assert test checksum matches after the insert before the last line + assert: + that: + - result.stat.checksum == '49d988ad97fb4cce3ad795c8459f1cde231a891b' + +- name: replace a line with backrefs + # 05 + win_lineinfile: dest={{ remote_tmp_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 + ansible.windows.win_stat: path={{ remote_tmp_dir }}/test.txt + register: result + +- name: assert test checksum matches after backref line was replaced + assert: + that: + - result.stat.checksum == '6f9c2128f4c886f3c40c1f1cf50241d74f160437' + +- name: remove the middle line + # 06 + win_lineinfile: dest={{ remote_tmp_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 + ansible.windows.win_stat: path={{ remote_tmp_dir }}/test.txt + register: result + +- name: assert test checksum matches after the middle line was removed + assert: + that: + - result.stat.checksum == '4b7910c84bd1177b9013f277c85d5be55f384a36' + +- name: run a validation script that succeeds + # 07 + win_lineinfile: dest={{ remote_tmp_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 + ansible.windows.win_stat: path={{ remote_tmp_dir }}/test.txt + register: result + +- name: assert test checksum matches after the validation succeeded + assert: + that: + - result.stat.checksum == '938afecdcde51fda42ddbea6f2e92876e710f289' + +- name: run a validation script that fails + # 08 + win_lineinfile: dest={{ remote_tmp_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 + ansible.windows.win_stat: path={{ remote_tmp_dir }}/test.txt + register: result + +- name: assert test checksum matches the previous after the validation failed + assert: + that: + - result.stat.checksum == '938afecdcde51fda42ddbea6f2e92876e710f289' + +- name: use create=yes + # 09 + win_lineinfile: dest={{ remote_tmp_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 + ansible.windows.win_stat: path={{ remote_tmp_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 + ansible.windows.win_copy: src=testnoeof.txt dest={{ remote_tmp_dir }}/testnoeof.txt + register: result + +- name: testnoeof insert a line at the end of the file + # 10 + win_lineinfile: dest={{ remote_tmp_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 + ansible.windows.win_stat: path={{ remote_tmp_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 + # 11 + win_lineinfile: dest={{ remote_tmp_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 + ansible.windows.win_stat: path={{ remote_tmp_dir }}/test.txt + register: result + +- name: assert test checksum matches after inserting multiple lines + assert: + that: + - result.stat.checksum == 'd10d70cb77a16fb54f143bccbf711f3177acd310' + + + +# Test EOF with empty file to make sure no unnecessary newline is added +- name: testempty deploy the testempty file for lineinfile + ansible.windows.win_copy: src=testempty.txt dest={{ remote_tmp_dir }}/testempty.txt + register: result + +- name: testempty insert a line at the end of the file + # 12 + win_lineinfile: dest={{ remote_tmp_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 + ansible.windows.win_stat: path={{ remote_tmp_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 + # 13 + win_lineinfile: dest={{ remote_tmp_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 + ansible.windows.win_stat: path={{ remote_tmp_dir }}/test.txt + register: result + +- name: assert test checksum matches after backref line was replaced + assert: + that: + - result.stat.checksum == '9c1a1451b50665e59be666c5d8c08fb99603d4f1' + +################################################################### +# issue 8535 + +- name: create a new file for testing quoting issues + # 14 + ansible.windows.win_copy: src=test_quoting.txt dest={{ remote_tmp_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={{ remote_tmp_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 + ansible.windows.win_stat: path={{ remote_tmp_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 + # 15 + win_lineinfile: dest={{ remote_tmp_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 + ansible.windows.win_stat: path={{ remote_tmp_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 + # 16 + win_lineinfile: dest={{ remote_tmp_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 + ansible.windows.win_stat: path={{ remote_tmp_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 + # 17 + win_lineinfile: dest={{ remote_tmp_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 + ansible.windows.win_stat: path={{ remote_tmp_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 + # 18 + win_lineinfile: dest={{ remote_tmp_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 + ansible.windows.win_stat: path={{ remote_tmp_dir }}/test_windows_sep.txt + register: result + +- name: assert the file checksum matches expected checksum + assert: + that: + - result.stat.checksum == '6c6f51f98eb499852fbb7ef3b212c26752c25c31' + + +- name: Create unix test file with initial line + # 19 + win_lineinfile: dest={{ remote_tmp_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 + ansible.windows.win_stat: path={{ remote_tmp_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 + # 20 + win_lineinfile: dest={{ remote_tmp_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 + ansible.windows.win_stat: path={{ remote_tmp_dir }}/test_unix_sep.txt + register: result + +- name: assert the file checksum matches expected checksum + assert: + that: + - result.stat.checksum == '4aa2ad771bb1453406760eadee8234265d599dcf' + + +# 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 + # 21 + win_lineinfile: dest={{ remote_tmp_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 + ansible.windows.win_stat: path={{ remote_tmp_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 + # 22 + win_lineinfile: dest={{ remote_tmp_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 + ansible.windows.win_stat: path={{ remote_tmp_dir }}/test_auto_utf8.txt + register: result + +- name: assert the file checksum matches + assert: + that: + - result.stat.checksum == 'aeb246a40f614889534f4983f47c5567625ade53' + + +# UTF-8 explicit (with BOM) +- name: Test create file with explicit utf-8 encoding results in utf-8 with a BOM + # 23 + win_lineinfile: dest={{ remote_tmp_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 + ansible.windows.win_stat: path={{ remote_tmp_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 + # 24 + win_lineinfile: dest={{ remote_tmp_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 + ansible.windows.win_stat: path={{ remote_tmp_dir }}/test_utf8.txt + register: result + +- name: assert the file checksum matches + assert: + that: + - result.stat.checksum == '64c62066d06ea6c807a8fe98bc40c4903cf4c119' + + +# UTF-16 explicit +- name: Test create file with explicit utf-16 encoding + # 25 + win_lineinfile: dest={{ remote_tmp_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 + ansible.windows.win_stat: path={{ remote_tmp_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 + # 26 + win_lineinfile: dest={{ remote_tmp_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 + ansible.windows.win_stat: path={{ remote_tmp_dir }}/test_utf16.txt + register: result + +- name: assert the file checksum matches + assert: + that: + - result.stat.checksum == '7f790f323e496b7138883a3634514cc2a3426919' + +# UTF-32 explicit +- name: Test create file with explicit utf-32 encoding + # 27 + win_lineinfile: dest={{ remote_tmp_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 + ansible.windows.win_stat: path={{ remote_tmp_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 + # 28 + win_lineinfile: dest={{ remote_tmp_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 + ansible.windows.win_stat: path={{ remote_tmp_dir }}/test_utf32.txt + register: result + +- name: assert the file checksum matches + assert: + that: + - result.stat.checksum == 'd13e135c7d466cc2f985d72f12ffaa73567772e6' + +######################################################################### +# issue #33858 +# \r\n causes line break instead of printing literally which breaks paths. + +- name: create testing file + ansible.windows.win_copy: + src: test_linebreak.txt + dest: "{{ remote_tmp_dir }}/test_linebreak.txt" + +- name: stat the test file + ansible.windows.win_stat: + path: "{{ remote_tmp_dir }}/test_linebreak.txt" + register: result + +- 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 + # 29 + win_lineinfile: + dest: "{{ remote_tmp_dir }}/test_linebreak.txt" + line: c:\return\new + register: result_literal + +- name: insert path "c:\return\new" to test file, will cause line breaks + # 30 + win_lineinfile: + dest: "{{ remote_tmp_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 + ansible.windows.win_stat: + path: "{{ remote_tmp_dir }}/test_linebreak.txt" + register: result + +- name: assert that one line is literal and the other has breaks + assert: + that: + - result.stat.checksum == 'd2dfd11bc70526ff13a91153c76a7ae5595a845b' + +- name: Get current working directory + win_shell: $pwd.Path + register: pwd + +- name: create a relative tmp dir + ansible.windows.win_tempfile: + path: "{{ pwd.stdout | trim | win_dirname }}" + state: directory + register: relative_tmp_dir + +- name: Check that relative paths work + block: + - name: deploy the test file for lineinfile + ansible.windows.win_copy: src=test.txt dest={{ relative_tmp_dir.path }}/test.txt + register: result + + - name: assert that the test file was deployed + assert: + that: + - "result.changed == true" + + - name: stat the test file + ansible.windows.win_stat: path={{ relative_tmp_dir.path }}/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 + # 31 + win_lineinfile: dest=../{{ relative_tmp_dir.path | win_basename }}/test.txt state=present line="New line at the beginning" insertbefore="BOF" backup=yes + register: result + + - name: check backup_file + ansible.windows.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 + ansible.windows.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 + ansible.windows.win_stat: path={{ relative_tmp_dir.path }}/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 == 'c61233b0ee2038aab41b5f30683c57b2a013b376' + + always: + - ansible.windows.win_file: + path: "{{ relative_tmp_dir.path }}" + state: absent diff --git a/ansible_collections/community/windows/tests/integration/targets/win_listen_ports_facts/aliases b/ansible_collections/community/windows/tests/integration/targets/win_listen_ports_facts/aliases new file mode 100644 index 000000000..4f4664b68 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_listen_ports_facts/aliases @@ -0,0 +1 @@ +shippable/windows/group5 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_listen_ports_facts/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_listen_ports_facts/tasks/main.yml new file mode 100644 index 000000000..77649d8f1 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_listen_ports_facts/tasks/main.yml @@ -0,0 +1,45 @@ +# This file is part of Ansible + +# Copyright: (c) 2022, DataDope (@datadope-io) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: Gather facts with invalid tcp_filter state and null date format + win_listen_ports_facts: + tcp_filter: + - Error + date_format: null + register: invalid_state_date_run + ignore_errors: True + +- name: Gather facts with only some invalid tcp_filter states and null date format + win_listen_ports_facts: + tcp_filter: + - Listen + - Closed + - Error + date_format: null + register: partial_invalid_state_date_run + ignore_errors: True + +- name: Gather facts with multiple filters and custom date format + win_listen_ports_facts: + tcp_filter: + - Listen + - Closed + date_format: yyyy + register: custom_state_date_run + ignore_errors: True + +- name: Gather facts with default filters and date format + win_listen_ports_facts: + register: default_run + ignore_errors: True + +- assert: + that: + - invalid_state_date_run.failed is defined and invalid_state_date_run.failed + - partial_invalid_state_date_run.failed is defined and partial_invalid_state_date_run.failed + - custom_state_date_run.failed is not defined or not custom_state_date_run.failed + - default_run.failed is not defined or not default_run.failed + - tcp_listen is defined + - udp_listen is defined diff --git a/ansible_collections/community/windows/tests/integration/targets/win_mapped_drive/aliases b/ansible_collections/community/windows/tests/integration/targets/win_mapped_drive/aliases new file mode 100644 index 000000000..4f4664b68 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_mapped_drive/aliases @@ -0,0 +1 @@ +shippable/windows/group5 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_mapped_drive/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_mapped_drive/defaults/main.yml new file mode 100644 index 000000000..6e8ed002e --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_mapped_drive/defaults/main.yml @@ -0,0 +1,9 @@ +test_win_mapped_drive_letter: M +test_win_mapped_drive_path: share1 +test_win_mapped_drive_path2: share2 + +test_win_mapped_drive_local_path: C:\ansible\win_mapped_drive\share1 +test_win_mapped_drive_local_path2: C:\ansible\win_mapped_drive\share2 + +test_win_mapped_drive_temp_user: TestMappedUser +test_win_mapped_drive_temp_password: aZ293jgkdslgj4 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_mapped_drive/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_mapped_drive/tasks/main.yml new file mode 100644 index 000000000..19d5acb2c --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_mapped_drive/tasks/main.yml @@ -0,0 +1,99 @@ +--- +# test setup +- name: gather facts required by the tests + ansible.windows.setup: + gather_subset: platform + +- name: ensure mapped drive is deleted before test + win_mapped_drive: + letter: '{{test_win_mapped_drive_letter}}' + state: absent + +- name: ensure temp mapped drive user exist + ansible.windows.win_user: + name: '{{test_win_mapped_drive_temp_user}}' + password: '{{test_win_mapped_drive_temp_password}}' + state: present + groups: + - Administrators + +- name: ensure temp folders exist + ansible.windows.win_file: + path: '{{item}}' + state: directory + with_items: + - '{{test_win_mapped_drive_local_path}}' + - '{{test_win_mapped_drive_local_path2}}' + +# can't use win_share as it doesnt't support Server 2008 and 2008 R2 +- name: ensure shares exist + ansible.windows.win_shell: $share = Get-WmiObject -Class Win32_Share | Where-Object { $_.Name -eq '{{item.name}}' }; if (-not $share) { $share = [wmiClass]'Win32_Share'; $share.Create('{{item.path}}', '{{item.name}}', 0) } + with_items: + - { name: '{{test_win_mapped_drive_path}}', path: '{{test_win_mapped_drive_local_path}}' } + - { name: '{{test_win_mapped_drive_path2}}', path: '{{test_win_mapped_drive_local_path2}}' } + +# This ensures we test out the split token/become behaviour +- name: ensure builtin Administrator has a split token + ansible.windows.win_regedit: + path: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System + name: FilterAdministratorToken + data: 1 + type: dword + register: admin_uac + +- name: reboot to apply Admin approval mode setting + ansible.windows.win_reboot: + when: admin_uac is changed + +- block: + # tests + - include_tasks: tests.yml + + # test cleanup + always: + - name: remove stored credential + win_credential: + name: '{{ ansible_hostname }}' + type: domain_password + state: absent + vars: + ansible_become: yes + ansible_become_method: runas + ansible_become_user: '{{ ansible_user }}' + ansible_become_pass: '{{ ansible_password }}' + + - name: ensure mapped drive is deleted at the end of the test + win_mapped_drive: + letter: '{{test_win_mapped_drive_letter}}' + state: absent + + - name: ensure shares are removed + ansible.windows.win_shell: $share = Get-WmiObject -Class Win32_Share | Where-Object { $_.Name -eq '{{item}}' }; if ($share) { $share.Delete() } + with_items: + - '{{test_win_mapped_drive_path}}' + - '{{test_win_mapped_drive_path2}}' + + - name: ensure temp folders are deleted + ansible.windows.win_file: + path: '{{item}}' + state: absent + with_items: + - '{{test_win_mapped_drive_local_path}}' + - '{{test_win_mapped_drive_local_path2}}' + + - name: ensure temp mapped driver user is deleted + ansible.windows.win_user: + name: '{{test_win_mapped_drive_temp_user}}' + state: absent + + - name: disable Admin approval mode if changed in test + ansible.windows.win_regedit: + path: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System + name: FilterAdministratorToken + data: 0 + type: dword + when: admin_uac is changed + + - name: reboot to apply Admin approval mode setting + ansible.windows.win_reboot: + when: admin_uac is changed diff --git a/ansible_collections/community/windows/tests/integration/targets/win_mapped_drive/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_mapped_drive/tasks/tests.yml new file mode 100644 index 000000000..2529b8ba1 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_mapped_drive/tasks/tests.yml @@ -0,0 +1,344 @@ +--- +- name: fail with invalid path + win_mapped_drive: + letter: invalid + state: absent + register: fail_invalid_letter + failed_when: "fail_invalid_letter.msg != 'letter must be a single letter from A-Z, was: invalid'" + +- name: fail without specify path when creating drive + win_mapped_drive: + letter: '{{test_win_mapped_drive_letter}}' + state: present + register: fail_path_missing + failed_when: "fail_path_missing.msg != 'state is present but all of the following are missing: path'" + +- name: fail when specifying letter with existing physical path + win_mapped_drive: + letter: c + path: \\{{ansible_hostname}}\{{test_win_mapped_drive_path}} + state: present + register: fail_local_letter + failed_when: fail_local_letter.msg != 'failed to create mapped drive c, this letter is in use and is pointing to a non UNC path' + +- name: create mapped drive check + win_mapped_drive: + letter: '{{test_win_mapped_drive_letter}}' + path: \\{{ansible_hostname}}\{{test_win_mapped_drive_path}} + state: present + register: create_drive_check + check_mode: yes + +- name: get actual of create mapped drive check + ansible.windows.win_command: 'net use {{test_win_mapped_drive_letter}}:' # Get-PSDrive/Get-WmiObject/Get-CimInstance doesn't work over WinRM + register: create_drive_actual_check + failed_when: False + +- name: assert create mapped drive check + assert: + that: + - create_drive_check is changed + - create_drive_actual_check.rc == 2 # should fail with this error code when it isn't found + +- name: create mapped drive + win_mapped_drive: + letter: '{{test_win_mapped_drive_letter}}' + path: \\{{ansible_hostname}}\{{test_win_mapped_drive_path}} + state: present + register: create_drive + +- name: get actual of create mapped drive + ansible.windows.win_command: 'net use {{test_win_mapped_drive_letter}}:' + register: create_drive_actual + +- name: assert create mapped drive + assert: + that: + - create_drive is changed + - create_drive_actual.rc == 0 + - create_drive_actual.stdout_lines[1] == "Remote name \\\\{{ansible_hostname}}\\{{test_win_mapped_drive_path}}" + +- name: create mapped drive again + win_mapped_drive: + letter: '{{test_win_mapped_drive_letter}}' + path: \\{{ansible_hostname}}\{{test_win_mapped_drive_path}} + state: present + register: create_drive_again + +- name: assert create mapped drive again + assert: + that: + - create_drive_again is not changed + +- name: change mapped drive target check + win_mapped_drive: + letter: '{{test_win_mapped_drive_letter}}' + path: \\{{ansible_hostname}}\{{test_win_mapped_drive_path2}} + state: present + register: change_drive_target_check + check_mode: yes + +- name: get actual of change mapped drive target check + ansible.windows.win_command: 'net use {{test_win_mapped_drive_letter}}:' + register: change_drive_target_actual_check + +- name: assert change mapped drive target check + assert: + that: + - change_drive_target_check is changed + - change_drive_target_actual_check.rc == 0 + - change_drive_target_actual_check.stdout_lines[1] == "Remote name \\\\{{ansible_hostname}}\\{{test_win_mapped_drive_path}}" + +- name: change mapped drive target + win_mapped_drive: + letter: '{{test_win_mapped_drive_letter}}' + path: \\{{ansible_hostname}}\{{test_win_mapped_drive_path2}} + state: present + register: change_drive_target + +- name: get actual of change mapped drive target + ansible.windows.win_command: 'net use {{test_win_mapped_drive_letter}}:' + register: change_drive_target_actual + +- name: assert change mapped drive target + assert: + that: + - change_drive_target is changed + - change_drive_target_actual.rc == 0 + - change_drive_target_actual.stdout_lines[1] == "Remote name \\\\{{ansible_hostname}}\\{{test_win_mapped_drive_path2}}" + +- name: fail to delete mapped drive if target doesn't match + win_mapped_drive: + letter: '{{test_win_mapped_drive_letter}}' + path: \\{{ansible_hostname}}\{{test_win_mapped_drive_path}} + state: absent + register: fail_delete_incorrect_target + failed_when: fail_delete_incorrect_target.msg != 'did not delete mapped drive ' + test_win_mapped_drive_letter + ', the target path is pointing to a different location at \\\\' + ansible_hostname + '\\' + test_win_mapped_drive_path2 + +- name: delete mapped drive check + win_mapped_drive: + letter: '{{test_win_mapped_drive_letter}}' + path: \\{{ansible_hostname}}\{{test_win_mapped_drive_path2}} + state: absent + register: delete_drive_check + check_mode: yes + +- name: get actual of delete mapped drive check + ansible.windows.win_command: 'net use {{test_win_mapped_drive_letter}}:' + register: delete_drive_actual_check + +- name: assert delete mapped drive check + assert: + that: + - delete_drive_check is changed + - delete_drive_actual_check.rc == 0 + - delete_drive_actual_check.stdout_lines[1] == "Remote name \\\\{{ansible_hostname}}\\{{test_win_mapped_drive_path2}}" + +- name: delete mapped drive + win_mapped_drive: + letter: '{{test_win_mapped_drive_letter}}' + path: \\{{ansible_hostname}}\{{test_win_mapped_drive_path2}} + state: absent + register: delete_drive + +- name: get actual of delete mapped drive + ansible.windows.win_command: 'net use {{test_win_mapped_drive_letter}}:' + register: delete_drive_actual + failed_when: False + +- name: assert delete mapped drive + assert: + that: + - delete_drive is changed + - delete_drive_actual.rc == 2 + +- name: delete mapped drive again + win_mapped_drive: + letter: '{{test_win_mapped_drive_letter}}' + path: \\{{ansible_hostname}}\{{test_win_mapped_drive_path2}} + state: absent + register: delete_drive_again + +- name: assert delete mapped drive again + assert: + that: + - delete_drive_again is not changed + +# not much we can do to test out the credentials except that it sets it, winrm +# makes it hard to actually test out we can still access the mapped drive +- name: map drive with current credentials check + win_mapped_drive: + letter: '{{test_win_mapped_drive_letter}}' + path: \\{{ansible_hostname}}\{{test_win_mapped_drive_path}} + state: present + username: '{{ansible_hostname}}\{{test_win_mapped_drive_temp_user}}' + password: '{{test_win_mapped_drive_temp_password}}' + register: map_with_credentials_check + check_mode: yes + +- name: get actual of map drive with current credentials check + ansible.windows.win_command: 'net use {{test_win_mapped_drive_letter}}:' + register: map_with_credentials_actual_check + failed_when: False + +- name: assert map drive with current credentials check + assert: + that: + - map_with_credentials_check is changed + - map_with_credentials_actual_check.rc == 2 + +- name: map drive with current credentials + win_mapped_drive: + letter: '{{test_win_mapped_drive_letter}}' + path: \\{{ansible_hostname}}\{{test_win_mapped_drive_path}} + state: present + username: '{{ansible_hostname}}\{{test_win_mapped_drive_temp_user}}' + password: '{{test_win_mapped_drive_temp_password}}' + register: map_with_credentials + +- name: get actual of map drive with current credentials + ansible.windows.win_command: 'net use {{test_win_mapped_drive_letter}}:' + register: map_with_credentials_actual + +- name: get username of mapped network drive with credentials + ansible.windows.win_reg_stat: + path: HKCU:\Network\{{test_win_mapped_drive_letter}} + name: UserName + register: map_with_credential_actual_username + +- name: assert map drive with current credentials + assert: + that: + - map_with_credentials is changed + - map_with_credentials_actual.rc == 0 + - map_with_credential_actual_username.value == '' # we explicitly remove the username part in the module + +- name: map drive with current credentials again + win_mapped_drive: + letter: '{{test_win_mapped_drive_letter}}' + path: \\{{ansible_hostname}}\{{test_win_mapped_drive_path}} + state: present + username: '{{ansible_hostname}}\{{test_win_mapped_drive_temp_user}}' + password: '{{test_win_mapped_drive_temp_password}}' + register: map_with_credentials_again + +- name: assert map drive with current credentials again + assert: + that: + - not map_with_credentials_again is changed + +- name: delete mapped drive without path check + win_mapped_drive: + letter: '{{test_win_mapped_drive_letter}}' + state: absent + register: delete_without_path_check + check_mode: yes + +- name: get actual delete mapped drive without path check + ansible.windows.win_command: 'net use {{test_win_mapped_drive_letter}}:' + register: delete_without_path_actual_check + +- name: assert delete mapped drive without path check + assert: + that: + - delete_without_path_check is changed + - delete_without_path_actual_check.rc == 0 + +- name: delete mapped drive without path + win_mapped_drive: + letter: '{{test_win_mapped_drive_letter}}' + state: absent + register: delete_without_path + +- name: get actual delete mapped drive without path + ansible.windows.win_command: 'net use {{test_win_mapped_drive_letter}}:' + register: delete_without_path_actual + failed_when: False + +- name: assert delete mapped drive without path check + assert: + that: + - delete_without_path is changed + - delete_without_path_actual.rc == 2 + +- name: delete mapped drive without path again + win_mapped_drive: + letter: '{{test_win_mapped_drive_letter}}' + state: absent + register: delete_without_path_again + +- name: assert delete mapped drive without path check again + assert: + that: + - delete_without_path_again is not changed + +- name: store credential for test network account + win_credential: + name: '{{ ansible_hostname }}' + type: domain_password + username: '{{ test_win_mapped_drive_temp_user }}' + secret: '{{ test_win_mapped_drive_temp_password }}' + state: present + vars: &become_vars + ansible_become: yes + ansible_become_method: runas + ansible_become_user: '{{ ansible_user }}' + ansible_become_pass: '{{ ansible_password }}' + +- name: map drive with stored cred (check mode) + win_mapped_drive: + letter: '{{test_win_mapped_drive_letter}}' + path: \\{{ansible_hostname}}\{{test_win_mapped_drive_path}} + state: present + check_mode: yes + vars: *become_vars + register: map_with_stored_cred_check + +- name: get actual of map drive with stored cred (check mode) + ansible.windows.win_command: 'net use {{test_win_mapped_drive_letter}}:' + register: map_with_stored_cred_actual_check + failed_when: False + +- name: assert map drive with stored cred (check mode) + assert: + that: + - map_with_stored_cred_check is changed + - map_with_stored_cred_actual_check.rc == 2 + +- name: map drive with stored cred + win_mapped_drive: + letter: '{{test_win_mapped_drive_letter}}' + path: \\{{ansible_hostname}}\{{test_win_mapped_drive_path}} + state: present + vars: *become_vars + register: map_with_stored_cred + +- name: get actual of map drive with stored cred + ansible.windows.win_command: 'net use {{test_win_mapped_drive_letter}}:' + register: map_with_stored_cred_actual + +- name: get username of mapped network drive with stored cred + ansible.windows.win_reg_stat: + path: HKCU:\Network\{{test_win_mapped_drive_letter}} + name: UserName + register: map_with_stored_cred_actual_username + +- name: assert map drive with stored cred + assert: + that: + - map_with_stored_cred is changed + - map_with_stored_cred_actual.rc == 0 + - map_with_stored_cred_actual_username.value == '' + +- name: map drive with stored cred again + win_mapped_drive: + letter: '{{test_win_mapped_drive_letter}}' + path: \\{{ansible_hostname}}\{{test_win_mapped_drive_path}} + state: present + vars: *become_vars + register: map_with_stored_cred_again + +- name: assert map drive with stored cred again + assert: + that: + - not map_with_stored_cred_again is changed diff --git a/ansible_collections/community/windows/tests/integration/targets/win_msg/aliases b/ansible_collections/community/windows/tests/integration/targets/win_msg/aliases new file mode 100644 index 000000000..98b74ac98 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_msg/aliases @@ -0,0 +1,2 @@ +shippable/windows/group2 +unstable diff --git a/ansible_collections/community/windows/tests/integration/targets/win_msg/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_msg/tasks/main.yml new file mode 100644 index 000000000..17051239f --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_msg/tasks/main.yml @@ -0,0 +1,33 @@ +- name: Warn user + win_msg: + display_seconds: 10 + msg: Keep calm and carry on. + register: msg_result + +- name: Test msg_result + assert: + that: + - msg_result is not failed + - msg_result is changed + - msg_result.runtime_seconds < 10 + +- name: Warn user and wait for it + win_msg: + display_seconds: 5 + msg: Keep calm and carry on. + #to: '{{ ansible_user }}' + wait: yes + register: msg_wait_result + +- name: Test msg_wait_result + assert: + that: + - msg_wait_result is not failed + - msg_wait_result is changed + - msg_wait_result.runtime_seconds > 5 + +- name: fail to send a message > 255 characters + win_msg: + msg: "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456" + register: fail_too_long + failed_when: "fail_too_long.msg != 'msg length must be less than 256 characters, current length: 256'" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_net_adapter_feature/aliases b/ansible_collections/community/windows/tests/integration/targets/win_net_adapter_feature/aliases new file mode 100644 index 000000000..215e0b069 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_net_adapter_feature/aliases @@ -0,0 +1 @@ +shippable/windows/group4 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_net_adapter_feature/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_net_adapter_feature/meta/main.yml new file mode 100644 index 000000000..e7f499ee3 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_net_adapter_feature/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_win_device
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_net_adapter_feature/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_net_adapter_feature/tasks/main.yml new file mode 100644 index 000000000..a40e0e049 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_net_adapter_feature/tasks/main.yml @@ -0,0 +1,27 @@ +# We create the first adapter by depending on setup_win_device but we need a 2nd one for our tests +- name: create 2nd dummy network adapter device + win_device: + path: '%WinDir%\Inf\netloop.inf' + hardware_id: '*msloop' + state: present + register: network_device_name_raw2 + +- block: + - set_fact: + network_device_name2: '{{ network_device_name_raw2.name }}' + + - name: get name of the dummy network adapter + ansible.windows.win_shell: (Get-CimInstance -Class Win32_NetworkAdapter -Filter "Name='{{ network_device_name2 }}'").NetConnectionID + changed_when: False + register: network_adapter_name_raw2 + + - set_fact: + network_adapter_name2: '{{ network_adapter_name_raw2.stdout | trim }}' + + - include_tasks: tests.yml + + always: + - name: remove 2nd dummy network adapter device + win_device: + name: '{{ network_device_name_raw2.name }}' + state: absent diff --git a/ansible_collections/community/windows/tests/integration/targets/win_net_adapter_feature/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_net_adapter_feature/tasks/tests.yml new file mode 100644 index 000000000..188182da3 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_net_adapter_feature/tasks/tests.yml @@ -0,0 +1,284 @@ +- name: fail when interface isn't set + win_net_adapter_feature: + state: enabled + component_id: ms_tcpip6 + register: failed_task + failed_when: not failed_task is failed + +- name: fail when component_id isn't set + win_net_adapter_feature: + interface: '{{ network_adapter_name }}' + state: enabled + register: failed_task + failed_when: not failed_task is failed + +- name: fail when interface doesn't exist + win_net_adapter_feature: + interface: Ethernet10 + state: enabled + component_id: ms_tcpip6 + register: failed_task + failed_when: not failed_task is failed + +- name: fail when state is inapppropriate + win_net_adapter_feature: + interface: '{{ network_adapter_name }}' + state: disabled_inappropriate + component_id: ms_tcpip6 + register: failed_task + failed_when: not failed_task is failed + +- name: fail when component_id is inapppropriate + win_net_adapter_feature: + interface: '{{ network_adapter_name }}' + state: disabled + component_id: inappropriate_component + register: failed_task + failed_when: not failed_task is failed + +- name: disable ms_tcpip6 before execution of normal tests. + ansible.windows.win_shell: Disable-NetAdapterBinding -Name '{{ network_adapter_name }}' -ComponentID ms_tcpip6 + +- name: get the status of ms_tcpip6 before execution of normal tests. + ansible.windows.win_shell: (Get-NetAdapterBinding -Name '{{ network_adapter_name }}' -ComponentID ms_tcpip6).Enabled + register: before_normal_tests + +- name: assert that the status of ms_tcpip6 is 'disabled' before execution of normal tests. + assert: + that: + - before_normal_tests.stdout_lines[0] == 'False' + +- name: enable an interface of ms_tpip6 when state isn't set (because the default state is 'enabled') + win_net_adapter_feature: + interface: '{{ network_adapter_name }}' + component_id: ms_tcpip6 + register: enable_single + +- name: get the status of enable an interface of ms_tpip6 + ansible.windows.win_shell: (Get-NetAdapterBinding -Name '{{ network_adapter_name }}' -ComponentID ms_tcpip6).Enabled + register: enable_single_actual + +- name: assert that changed is true and the status turns 'enabled'. + assert: + that: + - enable_single is changed + - enable_single_actual.stdout_lines[0] == 'True' + +- name: enable an interface of ms_tcpip6 + win_net_adapter_feature: + interface: '{{ network_adapter_name }}' + state: enabled + component_id: ms_tcpip6 + register: enable_single_again + +- name: get the status of enable an interface of ms_tcpip6 + ansible.windows.win_shell: (Get-NetAdapterBinding -Name '{{ network_adapter_name }}' -ComponentID ms_tcpip6).Enabled + register: enable_single_again_actual + +- name: assert that changed is false and the status is still remained 'enabled'. + assert: + that: + - enable_single_again is not changed + - enable_single_again_actual.stdout_lines[0] == 'True' + +- name: disable an interface of ms_tcpip6 + win_net_adapter_feature: + interface: '{{ network_adapter_name }}' + state: disabled + component_id: ms_tcpip6 + register: disable_single + +- name: get the status of disable an interface of ms_tcpip6 + ansible.windows.win_shell: (Get-NetAdapterBinding -Name '{{ network_adapter_name }}' -ComponentID ms_tcpip6).Enabled + register: disable_single_actual + +- name: assert that changed is true and the status turns 'disabled'. + assert: + that: + - disable_single is changed + - disable_single_actual.stdout_lines[0] == 'False' + +- name: enable single component_id of multiple interface + win_net_adapter_feature: + interface: + - '{{ network_adapter_name }}' + - '{{ network_adapter_name2 }}' + state: enabled + component_id: ms_tcpip6 + register: enable_multiple + +- name: get the status of disable multiple interfaces of multiple interfaces - check mode + ansible.windows.win_shell: | + $info = Get-NetAdapterBinding -Name '{{ network_adapter_name }}', '{{ network_adapter_name2 }}' + @{ + '{{ network_adapter_name }}' = @{ + ms_tcpip6 = ($info | Where-Object { $_.Name -eq '{{ network_adapter_name }}' -and $_.ComponentID -eq 'ms_tcpip6'}).Enabled + } + '{{ network_adapter_name2 }}' = @{ + ms_tcpip6 = ($info | Where-Object { $_.Name -eq '{{ network_adapter_name2 }}' -and $_.ComponentID -eq 'ms_tcpip6'}).Enabled + } + } | ConvertTo-Json + register: enable_multiple_actual + +- name: assert that changed is true and each status turns 'enabled'. + assert: + that: + - enable_multiple is changed + - (enable_multiple_actual.stdout | from_json)[network_adapter_name]['ms_tcpip6'] == True + - (enable_multiple_actual.stdout | from_json)[network_adapter_name2]['ms_tcpip6'] == True + +- name: disable multiple component_id of multiple interfaces + win_net_adapter_feature: + interface: + - '{{ network_adapter_name }}' + - '{{ network_adapter_name2 }}' + state: disabled + component_id: + - ms_tcpip6 + - ms_server + register: mutliple_multiple_disable + +- name: get the status of disable multiple component_id of multiple interfaces + ansible.windows.win_shell: | + $info = Get-NetAdapterBinding -Name '{{ network_adapter_name }}', '{{ network_adapter_name2 }}' + @{ + '{{ network_adapter_name }}' = @{ + ms_tcpip6 = ($info | Where-Object { $_.Name -eq '{{ network_adapter_name }}' -and $_.ComponentID -eq 'ms_tcpip6'}).Enabled + ms_server = ($info | Where-Object { $_.Name -eq '{{ network_adapter_name }}' -and $_.ComponentID -eq 'ms_server'}).Enabled + } + '{{ network_adapter_name2 }}' = @{ + ms_tcpip6 = ($info | Where-Object { $_.Name -eq '{{ network_adapter_name2 }}' -and $_.ComponentID -eq 'ms_tcpip6'}).Enabled + ms_server = ($info | Where-Object { $_.Name -eq '{{ network_adapter_name2 }}' -and $_.ComponentID -eq 'ms_server'}).Enabled + } + } | ConvertTo-Json + register: mutliple_multiple_disable_actual + +- name: assert that changed is true and each status turns 'disabled'. + assert: + that: + - mutliple_multiple_disable is changed + - (mutliple_multiple_disable_actual.stdout | from_json)[network_adapter_name]['ms_tcpip6'] == False + - (mutliple_multiple_disable_actual.stdout | from_json)[network_adapter_name]['ms_server'] == False + - (mutliple_multiple_disable_actual.stdout | from_json)[network_adapter_name2]['ms_tcpip6'] == False + - (mutliple_multiple_disable_actual.stdout | from_json)[network_adapter_name2]['ms_server'] == False + +- name: enable multiple interfaces of multiple interfaces + win_net_adapter_feature: + interface: + - '{{ network_adapter_name }}' + - '{{ network_adapter_name2 }}' + state: enabled + component_id: + - ms_tcpip6 + - ms_server + register: multiple_multiple_enable + +- name: get the status of disable multiple component_id of multiple interfaces + ansible.windows.win_shell: | + $info = Get-NetAdapterBinding -Name '{{ network_adapter_name }}', '{{ network_adapter_name2 }}' + @{ + '{{ network_adapter_name }}' = @{ + ms_tcpip6 = ($info | Where-Object { $_.Name -eq '{{ network_adapter_name }}' -and $_.ComponentID -eq 'ms_tcpip6'}).Enabled + ms_server = ($info | Where-Object { $_.Name -eq '{{ network_adapter_name }}' -and $_.ComponentID -eq 'ms_server'}).Enabled + } + '{{ network_adapter_name2 }}' = @{ + ms_tcpip6 = ($info | Where-Object { $_.Name -eq '{{ network_adapter_name2 }}' -and $_.ComponentID -eq 'ms_tcpip6'}).Enabled + ms_server = ($info | Where-Object { $_.Name -eq '{{ network_adapter_name2 }}' -and $_.ComponentID -eq 'ms_server'}).Enabled + } + } | ConvertTo-Json + register: multiple_multiple_enable_actual + +- name: assert that changed is true and each status turns 'enabled'. + assert: + that: + - multiple_multiple_enable is changed + - (multiple_multiple_enable_actual.stdout | from_json)[network_adapter_name]['ms_tcpip6'] == True + - (multiple_multiple_enable_actual.stdout | from_json)[network_adapter_name]['ms_server'] == True + - (multiple_multiple_enable_actual.stdout | from_json)[network_adapter_name2]['ms_tcpip6'] == True + - (multiple_multiple_enable_actual.stdout | from_json)[network_adapter_name2]['ms_server'] == True + +- name: enable multiple interfaces of multiple interfaces - idempotent + win_net_adapter_feature: + interface: + - '{{ network_adapter_name }}' + - '{{ network_adapter_name2 }}' + state: enabled + component_id: + - ms_tcpip6 + - ms_server + register: multiple_multiple_enable_again + +- name: assert that changed is true and each status turns 'enabled'. + assert: + that: + - multiple_multiple_enable_again is not changed + +- name: disable multiple interfaces of multiple interfaces - check mode + win_net_adapter_feature: + interface: + - '{{ network_adapter_name }}' + - '{{ network_adapter_name2 }}' + state: disabled + component_id: + - ms_tcpip6 + - ms_server + check_mode: yes + register: mutliple_multiple_disable_check + +- name: get the status of disable multiple interfaces of multiple interfaces - check mode + ansible.windows.win_shell: | + $info = Get-NetAdapterBinding -Name '{{ network_adapter_name }}', '{{ network_adapter_name2 }}' + @{ + '{{ network_adapter_name }}' = @{ + ms_tcpip6 = ($info | Where-Object { $_.Name -eq '{{ network_adapter_name }}' -and $_.ComponentID -eq 'ms_tcpip6'}).Enabled + ms_server = ($info | Where-Object { $_.Name -eq '{{ network_adapter_name }}' -and $_.ComponentID -eq 'ms_server'}).Enabled + } + '{{ network_adapter_name2 }}' = @{ + ms_tcpip6 = ($info | Where-Object { $_.Name -eq '{{ network_adapter_name2 }}' -and $_.ComponentID -eq 'ms_tcpip6'}).Enabled + ms_server = ($info | Where-Object { $_.Name -eq '{{ network_adapter_name2 }}' -and $_.ComponentID -eq 'ms_server'}).Enabled + } + } | ConvertTo-Json + register: mutliple_multiple_disable_check_actual + +- name: assert disable multiple interfaces of multiple interfaces - check mode + assert: + that: + - mutliple_multiple_disable_check is changed + - (mutliple_multiple_disable_check_actual.stdout | from_json)[network_adapter_name]['ms_tcpip6'] == True + - (mutliple_multiple_disable_check_actual.stdout | from_json)[network_adapter_name]['ms_server'] == True + - (mutliple_multiple_disable_check_actual.stdout | from_json)[network_adapter_name2]['ms_tcpip6'] == True + - (mutliple_multiple_disable_check_actual.stdout | from_json)[network_adapter_name2]['ms_server'] == True + +- name: disable all the interfaces of ms_tcpip6 + win_net_adapter_feature: + interface: '*' + state: disabled + component_id: ms_tcpip6 + register: disable_all + +- name: get the status of disable all the interfaces of ms_tcpip6 + ansible.windows.win_shell: (Get-NetAdapterBinding -Name * -ComponentID ms_tcpip6).Enabled -eq $true + register: disable_all_actual + +- name: assert that changed is true and each status turns 'enabled'. + assert: + that: + - disable_all is changed + - disable_all_actual.stdout_lines == [] + +- name: enable all the interfaces of ms_tcpip6 + win_net_adapter_feature: + interface: '*' + state: enabled + component_id: ms_tcpip6 + register: enable_all + +- name: get the status of enable all the interfaces of ms_tcpip6 + ansible.windows.win_shell: (Get-NetAdapterBinding -Name * -ComponentID ms_tcpip6).Enabled -eq $false + register: enable_all_actual + +- name: assert that changed is true and each status turns 'enabled'. + assert: + that: + - enable_all is changed + - enable_all_actual.stdout_lines == [] diff --git a/ansible_collections/community/windows/tests/integration/targets/win_netbios/aliases b/ansible_collections/community/windows/tests/integration/targets/win_netbios/aliases new file mode 100644 index 000000000..215e0b069 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_netbios/aliases @@ -0,0 +1 @@ +shippable/windows/group4 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_netbios/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_netbios/meta/main.yml new file mode 100644 index 000000000..e7f499ee3 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_netbios/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_win_device
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_netbios/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_netbios/tasks/main.yml new file mode 100644 index 000000000..2f96287bb --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_netbios/tasks/main.yml @@ -0,0 +1,30 @@ +# Test code for win_netbios module +# Copyright: (c) 2019, Thomas Moore <hi@tmmr.uk> + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +- name: ensure netbios is set to default to start with + win_netbios: + state: default + +- block: + - name: run tests + include_tasks: tests.yml + + always: + - name: set netbios back to default after tests + win_netbios: + state: default
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_netbios/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_netbios/tasks/tests.yml new file mode 100644 index 000000000..f0d7dd858 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_netbios/tasks/tests.yml @@ -0,0 +1,159 @@ +# Test code for win_netbios module +# Copyright: (c) 2019, Thomas Moore <hi@tmmr.uk> + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +- set_fact: + get_netbios_script: | + $adapter = Get-CimInstance -ClassName Win32_NetworkAdapter -Filter "NetConnectionID='{{ network_adapter_name }}'" + $config = Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration -Filter "Index=$($adapter.DeviceID)" + $config.TcpipNetbiosOptions + +- name: disable netbios single adapter (check mode) + win_netbios: + adapter_names: '{{ network_adapter_name }}' + state: disabled + register: set_single_check + check_mode: yes + +- name: get result of disable a single adapter test (check mode) + ansible.windows.win_shell: '{{ get_netbios_script }}' + changed_when: no + register: set_single_actual_check + +- name: assert disable a single adapter (check mode) + assert: + that: + - set_single_check is changed + - set_single_actual_check.stdout_lines == ["0"] + +- name: disable netbios single adapter + win_netbios: + adapter_names: '{{ network_adapter_name }}' + state: disabled + register: set_single + +- name: get result of disable a single adapter test + ansible.windows.win_shell: '{{ get_netbios_script }}' + changed_when: no + register: set_single_actual + +- name: assert disable a single adapter + assert: + that: + - set_single_check is changed + - set_single_actual.stdout_lines == ["2"] + +- name: fail with invalid network adapter name + win_netbios: + state: disabled + adapter_names: + - FakeAdapterName + register: invalid_adapter + failed_when: invalid_adapter.msg != "Not all of the target adapter names could be found on the system. No configuration changes have been made. FakeAdapterName" + +- name: disable netbios all adapters (check mode) + win_netbios: + state: disabled + check_mode: yes + register: disable_check + +- name: assert disable netbios (check mode) + assert: + that: + - disable_check.changed + +- name: disable netbios all adapters + win_netbios: + state: disabled + register: netbios_disable + +- name: assert netbios disabled + assert: + that: + - netbios_disable.changed + +- name: test disable idempotence + win_netbios: + state: disabled + register: netbios_disable + +- name: test disable idempotence + assert: + that: + - not netbios_disable.changed + +- name: enable netbios all adapters (check mode) + win_netbios: + state: enabled + check_mode: yes + register: enable_check + +- name: assert enable netbios all adapters (check mode) + assert: + that: + - enable_check.changed + +- name: enable netbios all adapters + win_netbios: + state: enabled + register: netbios_enable + +- name: assert netbios enabled + assert: + that: + - netbios_enable.changed + +- name: test enable idempotence + win_netbios: + state: enabled + register: netbios_enable + +- name: assert enable idempotence + assert: + that: + - not netbios_enable.changed + +- name: default netbios all adapters (check mode) + win_netbios: + state: default + check_mode: yes + register: default_check + +- name: assert default netbios (check mode) + assert: + that: + - default_check.changed + +- name: default netbios all adapters + win_netbios: + state: default + register: default_enable + +- name: assert netbios default all adapters + assert: + that: + - default_enable.changed + +- name: test default idempotence + win_netbios: + state: default + register: netbios_default + +- name: assert default idempotence + assert: + that: + - not netbios_default.changed
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_nssm/aliases b/ansible_collections/community/windows/tests/integration/targets/win_nssm/aliases new file mode 100644 index 000000000..423ce3910 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_nssm/aliases @@ -0,0 +1 @@ +shippable/windows/group2 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_nssm/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_nssm/defaults/main.yml new file mode 100644 index 000000000..005759965 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_nssm/defaults/main.yml @@ -0,0 +1,4 @@ +test_service_name: ansible_nssm_test +test_win_nssm_path: '{{ remote_tmp_dir }}\win_nssm' +test_win_nssm_username: testnssmuser +test_win_nssm_password: Password123!
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_nssm/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_nssm/meta/main.yml new file mode 100644 index 000000000..45806c8dc --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_nssm/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_remote_tmp_dir
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_nssm/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_nssm/tasks/main.yml new file mode 100644 index 000000000..7bd4b493c --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_nssm/tasks/main.yml @@ -0,0 +1,56 @@ +--- +- name: install NSSM + chocolatey.chocolatey.win_chocolatey: + name: NSSM + state: present + +- name: ensure testing folder exists + ansible.windows.win_file: + path: '{{test_win_nssm_path}}' + state: directory + +- name: create test user for service execution + ansible.windows.win_user: + name: '{{test_win_nssm_username}}' + password: '{{test_win_nssm_password}}' + state: present + groups_action: add + groups: + - Users + register: user_info + +# Run actual tests +- block: + - name: normalise test account name + ansible.windows.win_powershell: + parameters: + SID: '{{ user_info.sid }}' + script: | + [CmdletBinding()] + param ([String]$SID) + + $Ansible.Changed = $false + ([System.Security.Principal.SecurityIdentifier]$SID).Translate([System.Security.Principal.NTAccount]).Value + register: test_win_nssm_normalised_username + + - set_fact: + test_win_nssm_normalised_username: '{{ test_win_nssm_normalised_username.output[0] }}' + + - include_tasks: tests.yml + + always: + - name: ensure test service is absent + ansible.windows.win_service: + name: '{{ test_service_name }}' + state: absent + + - name: remove test user + ansible.windows.win_user: + name: '{{test_win_nssm_username}}' + state: absent + + - name: uninstall NSSM + chocolatey.chocolatey.win_chocolatey: + name: NSSM + state: absent + failed_when: false diff --git a/ansible_collections/community/windows/tests/integration/targets/win_nssm/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_nssm/tasks/tests.yml new file mode 100644 index 000000000..cf8f0cfbe --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_nssm/tasks/tests.yml @@ -0,0 +1,615 @@ +--- +- name: get register cmd that will get service info + set_fact: + test_service_cmd: | + $res = @{} + $srvobj = Get-WmiObject Win32_Service -Filter "Name=""$service""" | Select Name,DisplayName,Description,PathName,StartMode,StartName,State + if ($srvobj) { + $srvobj | Get-Member -MemberType *Property | % { $res.($_.name) = $srvobj.($_.name) } + + $startName = $res.StartName + $candidates = @(if ($startName -eq "LocalSystem") { + "NT AUTHORITY\SYSTEM" + } + elseif ($startName.Contains('\')) { + $nameSplit = $startName.Split('\', 2) + + if ($nameSplit[0] -eq '.') { + ,@($env:COMPUTERNAME, $nameSplit[1]) + $nameSplit[1] + } else { + ,$nameSplit + } + } + else { + $startName + }) + + $sid = for ($i = 0; $i -lt $candidates.Length; $i++) { + $candidate = $candidates[$i] + $ntAccount = New-Object -TypeName System.Security.Principal.NTAccount -ArgumentList $candidate + try { + $ntAccount.Translate([System.Security.Principal.SecurityIdentifier]) + break + } + catch [System.Security.Principal.IdentityNotMappedException] { + if ($i -eq ($candidates.Length - 1)) { + throw + } + continue + } + } + + $res.StartName = $sid.Translate([System.Security.Principal.NTAccount]).Value + + $res.Exists = $true + $res.Dependencies = @(Get-WmiObject -Query "Associators of {Win32_Service.Name=""$service""} Where AssocClass=Win32_DependentService" | select -ExpandProperty Name) + $res.Parameters = @{} + $srvkey = "HKLM:\SYSTEM\CurrentControlSet\Services\$service\Parameters" + Get-Item "$srvkey" | Select-Object -ExpandProperty property | % { $res.Parameters.$_ = (Get-ItemProperty -Path "$srvkey" -Name $_).$_} + } else { + $res.Exists = $false + } + ConvertTo-Json -InputObject $res -Compress + +- name: install service (check mode) + win_nssm: + name: '{{ test_service_name }}' + application: C:\Windows\System32\cmd.exe + state: present + register: install_service_check + check_mode: yes + +- name: get result of install service (check mode) + ansible.windows.win_shell: '$service = ''{{ test_service_name }}''; {{ test_service_cmd }}' + register: install_service_check_actual + +- name: assert results of install service (check mode) + assert: + that: + - install_service_check.changed == true + - (install_service_check_actual.stdout|from_json).Exists == false + +- name: install service + win_nssm: + name: '{{ test_service_name }}' + application: C:\Windows\System32\cmd.exe + state: present + register: install_service + +- name: get result of install service + ansible.windows.win_shell: '$service = ''{{ test_service_name }}''; {{ test_service_cmd }}' + register: install_service_actual + +- name: assert results of install service + assert: + that: + - install_service.changed == true + - (install_service_actual.stdout|from_json).Exists == true + - (install_service_actual.stdout|from_json).State == 'Stopped' + - (install_service_actual.stdout|from_json).StartMode == 'Auto' + - (install_service_actual.stdout|from_json).Parameters.Application == "C:\Windows\System32\cmd.exe" + - (install_service_actual.stdout|from_json).Parameters.AppDirectory == "C:\Windows\System32" + +- name: test install service (idempotent) + win_nssm: + name: '{{ test_service_name }}' + application: C:\Windows\System32\cmd.exe + state: present + register: install_service_again + +- name: get result of install service (idempotent) + ansible.windows.win_shell: '$service = ''{{ test_service_name }}''; {{ test_service_cmd }}' + register: install_service_again_actual + +- name: assert results of install service (idempotent) + assert: + that: + - install_service_again.changed == false + - (install_service_again_actual.stdout|from_json).Exists == true + - (install_service_again_actual.stdout|from_json).State == 'Stopped' + - (install_service_again_actual.stdout|from_json).StartMode == 'Auto' + - (install_service_again_actual.stdout|from_json).Parameters.Application == "C:\Windows\System32\cmd.exe" + - (install_service_again_actual.stdout|from_json).Parameters.AppDirectory == "C:\Windows\System32" + +- name: install and start service + win_nssm: + name: '{{ test_service_name }}' + application: C:\Windows\System32\cmd.exe + state: started + register: install_start_service + +- name: get result of install and start service + ansible.windows.win_shell: '$service = ''{{ test_service_name }}''; {{ test_service_cmd }}' + register: install_start_service_actual + +- name: assert results of install and start service + assert: + that: + - install_start_service.changed == true + - (install_start_service_actual.stdout|from_json).Exists == true + - (install_start_service_actual.stdout|from_json).State == 'Running' + - (install_start_service_actual.stdout|from_json).StartMode == 'Auto' + - (install_start_service_actual.stdout|from_json).Parameters.Application == "C:\Windows\System32\cmd.exe" + - (install_start_service_actual.stdout|from_json).Parameters.AppDirectory == "C:\Windows\System32" + +- name: install and start service with more parameters (check mode) + win_nssm: + name: '{{ test_service_name }}' + display_name: Ansible testing + description: win_nssm test service + application: C:\Windows\System32\cmd.exe + start_mode: manual + working_directory: '{{ test_win_nssm_path }}' + dependencies: 'tcpip,dnscache' + username: '{{ test_win_nssm_username }}' + password: '{{ test_win_nssm_password }}' + stdout_file: '{{ test_win_nssm_path }}\log.txt' + stderr_file: '{{ test_win_nssm_path }}\error.txt' + state: started + register: install_service_complex_check + check_mode: yes + +- name: get result of install and start service with more parameters (check mode) + ansible.windows.win_shell: '$service = ''{{ test_service_name }}''; {{ test_service_cmd }}' + register: install_service_complex_check_actual + +- name: assert results of install and start service with more parameters (check mode) + assert: + that: + - install_service_complex_check.changed == true + - (install_service_complex_check_actual.stdout|from_json).Exists == true + - (install_service_complex_check_actual.stdout|from_json).DisplayName == '{{ test_service_name }}' + - (install_service_complex_check_actual.stdout|from_json).Description is none + - (install_service_complex_check_actual.stdout|from_json).StartMode != 'Manual' + - (install_service_complex_check_actual.stdout|from_json).StartName != test_win_nssm_normalised_username + - (install_service_complex_check_actual.stdout|from_json).Parameters.Application == "C:\Windows\System32\cmd.exe" + - (install_service_complex_check_actual.stdout|from_json).Parameters.AppDirectory == "C:\Windows\System32" + - '"AppStdout" not in (install_service_complex_check_actual.stdout|from_json).Parameters' + - '"AppStderr" not in (install_service_complex_check_actual.stdout|from_json).Parameters' + - (install_service_complex_check_actual.stdout|from_json).Dependencies|length == 0 + +- name: install and start service with more parameters + win_nssm: + name: '{{ test_service_name }}' + display_name: Ansible testing + description: win_nssm test service + application: C:\Windows\System32\cmd.exe + start_mode: manual + working_directory: '{{ test_win_nssm_path }}' + dependencies: 'tcpip,dnscache' + username: '{{ test_win_nssm_username }}' + password: '{{ test_win_nssm_password }}' + stdout_file: '{{ test_win_nssm_path }}\log.txt' + stderr_file: '{{ test_win_nssm_path }}\error.txt' + state: started + register: install_service_complex + +- name: get result of install and start service with more parameters + ansible.windows.win_shell: '$service = ''{{ test_service_name }}''; {{ test_service_cmd }}' + register: install_service_complex_actual + +- name: assert results of install and start service with more parameters + assert: + that: + - install_service_complex.changed == true + - (install_service_complex_actual.stdout|from_json).Exists == true + - (install_service_complex_actual.stdout|from_json).DisplayName == 'Ansible testing' + - (install_service_complex_actual.stdout|from_json).Description == 'win_nssm test service' + - (install_service_complex_actual.stdout|from_json).State == 'Running' + - (install_service_complex_actual.stdout|from_json).StartMode == 'Manual' + - (install_service_complex_actual.stdout|from_json).StartName == test_win_nssm_normalised_username + - (install_service_complex_actual.stdout|from_json).Parameters.Application == "C:\Windows\System32\cmd.exe" + - (install_service_complex_actual.stdout|from_json).Parameters.AppDirectory == test_win_nssm_path + - (install_service_complex_actual.stdout|from_json).Parameters.AppStdout == test_win_nssm_path + '\\log.txt' + - (install_service_complex_actual.stdout|from_json).Parameters.AppStderr == test_win_nssm_path + '\\error.txt' + - (install_service_complex_actual.stdout|from_json).Dependencies|length == 2 + - '"Tcpip" in (install_service_complex_actual.stdout|from_json).Dependencies' + - '"Dnscache" in (install_service_complex_actual.stdout|from_json).Dependencies' + +- name: install and start service with more parameters (idempotent) + win_nssm: + name: '{{ test_service_name }}' + display_name: Ansible testing + description: win_nssm test service + application: C:\Windows\System32\cmd.exe + start_mode: manual + working_directory: '{{ test_win_nssm_path }}' + # Dependencies order should not trigger a change + dependencies: 'dnscache,tcpip' + username: '{{ test_win_nssm_username }}' + password: '{{ test_win_nssm_password }}' + stdout_file: '{{ test_win_nssm_path }}\log.txt' + stderr_file: '{{ test_win_nssm_path }}\error.txt' + state: started + register: install_service_complex_again + +- name: get result of install and start service with more parameters (idempotent) + ansible.windows.win_shell: '$service = ''{{ test_service_name }}''; {{ test_service_cmd }}' + register: install_service_complex_again_actual + +- name: assert results of install and start service with more parameters (idempotent) + assert: + that: + - install_service_complex_again.changed == false + - (install_service_complex_again_actual.stdout|from_json).Exists == true + - (install_service_complex_again_actual.stdout|from_json).DisplayName == 'Ansible testing' + - (install_service_complex_again_actual.stdout|from_json).Description == 'win_nssm test service' + - (install_service_complex_again_actual.stdout|from_json).State == 'Running' + - (install_service_complex_again_actual.stdout|from_json).StartMode == 'Manual' + - (install_service_complex_again_actual.stdout|from_json).StartName == test_win_nssm_normalised_username + - (install_service_complex_again_actual.stdout|from_json).Parameters.Application == "C:\Windows\System32\cmd.exe" + - (install_service_complex_again_actual.stdout|from_json).Parameters.AppDirectory == test_win_nssm_path + - (install_service_complex_again_actual.stdout|from_json).Parameters.AppStdout == test_win_nssm_path + '\\log.txt' + - (install_service_complex_again_actual.stdout|from_json).Parameters.AppStderr == test_win_nssm_path + '\\error.txt' + - (install_service_complex_again_actual.stdout|from_json).Dependencies|length == 2 + - '"Tcpip" in (install_service_complex_again_actual.stdout|from_json).Dependencies' + - '"Dnscache" in (install_service_complex_again_actual.stdout|from_json).Dependencies' + +- name: install service with string form parameters + win_nssm: + name: '{{ test_service_name }}' + application: C:\Windows\System32\cmd.exe + arguments: '-v -Dtest.str=value "C:\with space\\"' + state: present + register: str_params + +- name: get result of install service with string form parameters + ansible.windows.win_shell: '$service = ''{{ test_service_name }}''; {{ test_service_cmd }}' + register: str_params_actual + +- name: assert results of install service with string form parameters + assert: + that: + - str_params.changed == true + - (str_params_actual.stdout|from_json).Exists == true + - (str_params_actual.stdout|from_json).Parameters.Application == "C:\Windows\System32\cmd.exe" + # Expected value: -v -Dtest.str=value "C:\with space\\" (backslashes doubled for jinja) + - (str_params_actual.stdout|from_json).Parameters.AppParameters == '-v -Dtest.str=value "C:\\with space\\\\"' + +- name: install service with string form parameters (idempotent) + win_nssm: + name: '{{ test_service_name }}' + application: C:\Windows\System32\cmd.exe + arguments: '-v -Dtest.str=value "C:\with space\\"' + state: present + register: str_params_again + +- name: get result of install service with string form parameters (idempotent) + ansible.windows.win_shell: '$service = ''{{ test_service_name }}''; {{ test_service_cmd }}' + register: str_params_again_actual + +- name: assert results of install service with string form parameters (idempotent) + assert: + that: + - str_params_again.changed == false + - (str_params_again_actual.stdout|from_json).Exists == true + - (str_params_again_actual.stdout|from_json).Parameters.Application == "C:\Windows\System32\cmd.exe" + # Expected value: -v -Dtest.str=value "C:\with space\\" (backslashes doubled for jinja) + - (str_params_again_actual.stdout|from_json).Parameters.AppParameters == '-v -Dtest.str=value "C:\\with space\\\\"' + +- name: install service with extra environment vars (check mode) + win_nssm: + name: '{{ test_service_name }}' + application: C:\Windows\System32\cmd.exe + start_mode: manual + state: present + app_environment: + foo: bar + baz: 2 + register: install_service_appenv_check + check_mode: yes + +- name: get result of install service with extra environment vars (check mode) + ansible.windows.win_shell: nssm.exe get '{{ test_service_name }}' AppEnvironmentExtra + register: install_service_appenv_check_actual + + ## note: this could fail (in theory) when the service is not yet + ## installed (diff mode), but because of side effects of earlier + ## tests this will actually not fail in practice, however, it is + ## not a real issue in any case + failed_when: false + +- name: assert results of install service with extra environment vars (check mode) + assert: + that: + - install_service_appenv_check.changed == true + - install_service_appenv_check_actual.stdout == '\r\n' or install_service_appenv_check_actual.stdout == '' + +- name: install service with extra environment vars + win_nssm: + name: '{{ test_service_name }}' + application: C:\Windows\System32\cmd.exe + start_mode: manual + state: present + app_environment: + foo: bar + baz: 2 + register: install_service_appenv + +- name: get result of install service with extra environment vars + ansible.windows.win_shell: nssm.exe get '{{ test_service_name }}' AppEnvironmentExtra + register: install_service_appenv_actual + +- name: assert results of install service with extra environment vars + assert: + that: + - install_service_appenv.changed == true + - (install_service_appenv_actual.stdout_lines|length) == 3 + - (install_service_appenv_actual.stdout_lines[0]) == 'baz=2' + - (install_service_appenv_actual.stdout_lines[2]) == 'foo=bar' + +- name: install service with extra environment vars (idempotent) + win_nssm: + name: '{{ test_service_name }}' + application: C:\Windows\System32\cmd.exe + start_mode: manual + state: present + app_environment: + foo: bar + baz: 2 + register: install_service_appenv_idem + +- name: get result of install service with extra environment vars (idempotent) + ansible.windows.win_shell: nssm.exe get '{{ test_service_name }}' AppEnvironmentExtra + register: install_service_appenv_idem_actual + +- name: assert results of install service with extra environment vars (idempotent) + assert: + that: + - install_service_appenv_idem.changed == false + - (install_service_appenv_idem_actual.stdout_lines|length) == 3 + - (install_service_appenv_idem_actual.stdout_lines[0]) == 'baz=2' + - (install_service_appenv_idem_actual.stdout_lines[2]) == 'foo=bar' + +- name: install service dont change app_env if not explicitly requested + win_nssm: + name: '{{ test_service_name }}' + application: C:\Windows\System32\cmd.exe + start_mode: manual + state: present + register: install_service_appenv_implicit + +- name: get result of install service dont change app_env if not explicitly requested + ansible.windows.win_shell: nssm.exe get '{{ test_service_name }}' AppEnvironmentExtra + register: install_service_appenv_implicit_actual + +- name: assert results of install service dont change app_env if not explicitly requested + assert: + that: + - install_service_appenv_implicit.changed == false + - (install_service_appenv_implicit_actual.stdout_lines|length) == 3 + - (install_service_appenv_implicit_actual.stdout_lines[0]) == 'baz=2' + - (install_service_appenv_implicit_actual.stdout_lines[2]) == 'foo=bar' + +- name: install service resetting env vars + win_nssm: + name: '{{ test_service_name }}' + application: C:\Windows\System32\cmd.exe + start_mode: manual + state: present + app_environment: {} + register: install_service_reset_appenv + +- name: get result of install service resetting env vars + ansible.windows.win_shell: nssm.exe get '{{ test_service_name }}' AppEnvironmentExtra + register: install_service_reset_appenv_actual + +- name: assert results of install service resetting env vars + assert: + that: + - install_service_reset_appenv.changed == true + - install_service_reset_appenv_actual.stdout == '\r\n' + +# deprecated in 2.12 +- name: install service with dict-as-string parameters + win_nssm: + name: '{{ test_service_name }}' + application: C:\Windows\System32\cmd.exe + app_parameters: foo=true; -file.out=output.bat; -path=C:\with space\; -str=test"quotes; _=bar + register: mixed_params + +# deprecated in 2.12 +- name: get result of install service with dict-as-string parameters + ansible.windows.win_shell: '$service = ''{{ test_service_name }}''; {{ test_service_cmd }}' + register: mixed_params_actual + +# deprecated in 2.12 +- name: assert results of install service with dict-as-string parameters + assert: + that: + - mixed_params.changed == true + - (mixed_params_actual.stdout|from_json).Exists == true + - (mixed_params_actual.stdout|from_json).Parameters.Application == "C:\Windows\System32\cmd.exe" + # Expected value: bar -file.out output.bat -str "test\"quotes" foo true -path "C:\with space\\" (backslashes doubled for jinja) + - (mixed_params_actual.stdout|from_json).Parameters.AppParameters == 'bar -file.out output.bat -str "test\\"quotes" foo true -path "C:\\with space\\\\"' + +# deprecated in 2.12 +- name: install service with dict-as-string parameters (idempotent) + win_nssm: + name: '{{ test_service_name }}' + application: C:\Windows\System32\cmd.exe + app_parameters: foo=true; -file.out=output.bat; -path=C:\with space\; -str=test"quotes; _=bar + register: mixed_params_again + +# deprecated in 2.12 +- name: get result of install service with dict-as-string parameters (idempotent) + ansible.windows.win_shell: '$service = ''{{ test_service_name }}''; {{ test_service_cmd }}' + register: mixed_params_again_actual + +# deprecated in 2.12 +- name: assert results of install service with dict-as-string parameters (idempotent) + assert: + that: + - mixed_params_again.changed == false + - (mixed_params_again_actual.stdout|from_json).Exists == true + - (mixed_params_again_actual.stdout|from_json).Parameters.Application == "C:\Windows\System32\cmd.exe" + # Expected value: bar -file.out output.bat -str "test\"quotes" foo true -path "C:\with space\\" (backslashes doubled for jinja) + - (mixed_params_again_actual.stdout|from_json).Parameters.AppParameters == 'bar -file.out output.bat -str "test\\"quotes" foo true -path "C:\\with space\\\\"' + +- name: install service with list of parameters + win_nssm: + name: '{{ test_service_name }}' + application: C:\Windows\System32\cmd.exe + arguments: + - -foo=bar + - -day + # Test non-string value + - 14 + # Test if dot is not interpreted as separator (see #44079) + - -file.out + # Test if spaces are escaped + - C:\with space\output.bat + - -str + # Test if quotes and backslashes are escaped + - test"quotes\ + register: list_params + +- name: get result of install service with list of parameters + ansible.windows.win_shell: '$service = ''{{ test_service_name }}''; {{ test_service_cmd }}' + register: list_params_actual + +- name: assert results of install service with list of parameters + assert: + that: + - list_params.changed == true + - (list_params_actual.stdout|from_json).Exists == true + - (list_params_actual.stdout|from_json).Parameters.Application == "C:\Windows\System32\cmd.exe" + # Expected value: -foo=bar -day 14 -file.out "C:\with space\output.bat" -str "test\"quotes\\" (backslashes doubled for jinja) + - (list_params_actual.stdout|from_json).Parameters.AppParameters == '-foo=bar -day 14 -file.out "C:\\with space\\output.bat" -str "test\\"quotes\\\\"' + +- name: install service with list of parameters (idempotent) + win_nssm: + name: '{{ test_service_name }}' + application: C:\Windows\System32\cmd.exe + arguments: + - -foo=bar + - -day + - 14 + - -file.out + - C:\with space\output.bat + - -str + - test"quotes\ + register: list_params_again + +- name: get result of install service with list of parameters (idempotent) + ansible.windows.win_shell: '$service = ''{{ test_service_name }}''; {{ test_service_cmd }}' + register: list_params_again_actual + +- name: assert results of install service with list of parameters (idempotent) + assert: + that: + - list_params_again.changed == false + - (list_params_again_actual.stdout|from_json).Exists == true + - (list_params_again_actual.stdout|from_json).Parameters.Application == "C:\Windows\System32\cmd.exe" + # Expected value: -foo=bar -day 14 -file.out "C:\with space\output.bat" -str "test\"quotes\\" (backslashes doubled for jinja) + - (list_params_again_actual.stdout|from_json).Parameters.AppParameters == '-foo=bar -day 14 -file.out "C:\\with space\\output.bat" -str "test\\"quotes\\\\"' + +- name: set service username to SYSTEM + win_nssm: + name: '{{ test_service_name }}' + username: LocalSystem + application: C:\Windows\System32\cmd.exe + register: service_system + +- name: get service account for SYSTEM + ansible.windows.win_service_info: + name: '{{ test_service_name }}' + register: service_system_actual + +- name: assert set service username to SYSTEM + assert: + that: + - service_system is changed + - service_system_actual.services[0].username == 'NT AUTHORITY\\SYSTEM' + +- name: set service username to SYSTEM (idempotent) + win_nssm: + name: '{{ test_service_name }}' + username: SYSTEM + application: C:\Windows\System32\cmd.exe + register: service_system_again + +- name: assert set service username to SYSTEM (idempotent) + assert: + that: + - not service_system_again is changed + +- name: set service username to NETWORK SERVICE + win_nssm: + name: '{{ test_service_name }}' + username: NETWORK SERVICE + application: C:\Windows\System32\cmd.exe + register: service_network + +- name: get service account for NETWORK SERVICE + ansible.windows.win_service_info: + name: '{{ test_service_name }}' + register: service_network_actual + +- name: assert set service username to NETWORK SERVICE + assert: + that: + - service_network is changed + - service_network_actual.services[0].username == 'NT Authority\\NetworkService' + +- name: set service username to NETWORK SERVICE (idempotent) + win_nssm: + name: '{{ test_service_name }}' + username: NT AUTHORITY\NETWORK SERVICE + application: C:\Windows\System32\cmd.exe + register: service_network_again + +- name: assert set service username to NETWORK SERVICE (idempotent) + assert: + that: + - not service_network_again is changed + +- name: remove service (check mode) + win_nssm: + name: '{{ test_service_name }}' + state: absent + register: remove_service_check + check_mode: yes + +- name: get result of remove service (check mode) + ansible.windows.win_shell: '$service = ''{{ test_service_name }}''; {{ test_service_cmd }}' + register: remove_service_check_actual + +- name: assert results of remove service (check mode) + assert: + that: + - remove_service_check.changed == true + - (remove_service_check_actual.stdout|from_json).Exists == true + +- name: remove service + win_nssm: + name: '{{ test_service_name }}' + state: absent + register: remove_service + +- name: get result of remove service + ansible.windows.win_shell: '$service = ''{{ test_service_name }}''; {{ test_service_cmd }}' + register: remove_service_actual + +- name: assert results of remove service + assert: + that: + - remove_service.changed == true + - (remove_service_actual.stdout|from_json).Exists == false + +- name: remove service (idempotent) + win_nssm: + name: '{{ test_service_name }}' + state: absent + register: remove_service_again + +- name: get result of remove service (idempotent) + ansible.windows.win_shell: '$service = ''{{ test_service_name }}''; {{ test_service_cmd }}' + register: remove_service_again_actual + +- name: assert results of remove service (idempotent) + assert: + that: + - remove_service_again.changed == false + - (remove_service_again_actual.stdout|from_json).Exists == false diff --git a/ansible_collections/community/windows/tests/integration/targets/win_pagefile/aliases b/ansible_collections/community/windows/tests/integration/targets/win_pagefile/aliases new file mode 100644 index 000000000..a4da730ea --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_pagefile/aliases @@ -0,0 +1,2 @@ +shippable/windows/group3 +unstable diff --git a/ansible_collections/community/windows/tests/integration/targets/win_pagefile/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_pagefile/tasks/main.yml new file mode 100644 index 000000000..0cde27bfa --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_pagefile/tasks/main.yml @@ -0,0 +1,241 @@ +--- +# Get current pagefiles status +- name: Get original pagefile settings + win_pagefile: + state: query + register: original_pagefile_settings + +# Remove all original pagefiles +- name: Remove all original pagefiles + win_pagefile: + remove_all: true + register: remove_all_pagefiles + +# Test 1: Set c pagefile with inital and maximum size +- name: Set C pagefile as 1024-2048MB + win_pagefile: + remove_all: yes + drive: C + initial_size: 1024 + maximum_size: 2048 + override: yes + state: present + register: c_pagefile + +- name: Test set c pagefile + assert: + that: + - c_pagefile.changed == true + +- name: Query all pagefiles + win_pagefile: + state: query + register: pagefiles_query + +- name: Set fact for pagefile expected result + set_fact: + expected: + pagefiles: + - caption: "C:\\ 'pagefile.sys'" + description: "'pagefile.sys' @ C:\\" + initial_size: 1024 + maximum_size: 2048 + name: "C:\\pagefile.sys" + +- name: Test query - c pagefile 1024-2048 + assert: + that: + - pagefiles_query.changed == false + - pagefiles_query.pagefiles == expected.pagefiles + - pagefiles_query.automatic_managed_pagefiles == false + + +# Test 2: Remove c pagefile +- name: Remove C pagefile + win_pagefile: + drive: C + state: absent + register: delete_c_pagefile + +- name: Test removal of c pagefile + assert: + that: + - delete_c_pagefile.changed == true + +- name: Query all pagefiles + win_pagefile: + state: query + register: pagefiles_query + +- name: Set fact for pagefile expected result + set_fact: + expected: + pagefiles: [] + +- name: Test query - no c pagefile + assert: + that: + - pagefiles_query.changed == false + - pagefiles_query.pagefiles == expected.pagefiles + - pagefiles_query.automatic_managed_pagefiles == false + + +# Test 3: Set automatic managed pagefile as true +- name: Set automatic managed pagefiles as true + win_pagefile: + automatic: yes + register: set_automatic_true + +- name: Test removal of c pagefile + assert: + that: + - set_automatic_true.changed == true + - set_automatic_true.automatic_managed_pagefiles == true + + +# Test 4: Set c pagefile as system managed pagefile +- name: Set c pagefile as system managed pagefile + win_pagefile: + drive: C + system_managed: yes + state: present + register: c_pagefile_system_managed + +- name: Test set c pagefile as system managed + assert: + that: + - c_pagefile_system_managed.changed == true + +- name: Query all pagefiles + win_pagefile: + state: query + register: pagefiles_query + +- name: Set fact for pagefile expected result + set_fact: + expected: + pagefiles: + - caption: "C:\\ 'pagefile.sys'" + description: "'pagefile.sys' @ C:\\" + initial_size: 0 + maximum_size: 0 + name: "C:\\pagefile.sys" + +- name: Test query - c pagefile 0-0 (system managed) + assert: + that: + - pagefiles_query.changed == false + - pagefiles_query.pagefiles == expected.pagefiles + - pagefiles_query.automatic_managed_pagefiles == false + +# Test 5: Test no override +- name: Set c pagefile 1024-1024, no override + win_pagefile: + drive: C + initial_size: 1024 + maximum_size: 1024 + override: no + state: present + register: c_pagefile_no_override + +- name: Test set c pagefile no override + assert: + that: + - c_pagefile_no_override.changed == false + +- name: Query all pagefiles + win_pagefile: + state: query + register: pagefiles_query + +- name: Test query - c pagefile unchanged + assert: + that: + - pagefiles_query.changed == false + - pagefiles_query.pagefiles == expected.pagefiles + - pagefiles_query.automatic_managed_pagefiles == false + + +# Test 6: Test override +- name: Set c pagefile 1024-1024, override + win_pagefile: + drive: C + initial_size: 1024 + maximum_size: 1024 + state: present + register: c_pagefile_override + +- name: Test set c pagefile no override + assert: + that: + - c_pagefile_override.changed == true + + # Test 7: Test idempotent +- name: Set c pagefile 1024-1024, idempotent + win_pagefile: + drive: C + initial_size: 1024 + maximum_size: 1024 + override: no + state: present + register: c_pagefile_idempotent + +- name: Test set c pagefile idempotent + assert: + that: + - c_pagefile_idempotent.changed == false + +- name: Query all pagefiles + win_pagefile: + state: query + register: pagefiles_query + +- name: Set fact for pagefile expected result + set_fact: + expected: + pagefiles: + - caption: "C:\\ 'pagefile.sys'" + description: "'pagefile.sys' @ C:\\" + initial_size: 1024 + maximum_size: 1024 + name: "C:\\pagefile.sys" + +- name: Test query - c pagefile 1024-1024 + assert: + that: + - pagefiles_query.changed == false + - pagefiles_query.pagefiles == expected.pagefiles + - pagefiles_query.automatic_managed_pagefiles == false + +# Test 7: Remove all pagefiles +- name: Remove all pagefiles + win_pagefile: + remove_all: true + register: remove_all_pagefiles + +- name: Set fact for pagefile expected result + set_fact: + expected: + pagefiles: [] + +- name: Test query - no pagefiles + assert: + that: + - remove_all_pagefiles.changed == true + - remove_all_pagefiles.pagefiles == expected.pagefiles + - pagefiles_query.automatic_managed_pagefiles == false + +# Return all pagefile settings to its original state +- name: Remove all pagefiles and return automatic to its original state + win_pagefile: + remove_all: yes + automatic: "{{ original_pagefile_settings.automatic_managed_pagefiles }}" + +- name: Return all previous pagefiles settings + win_pagefile: + drive: "{{ item.name[0] }}" + initial_size: "{{ item.initial_size }}" + maximum_size: "{{ item.maximum_size }}" + test_path: no + state: present + with_items: "{{ original_pagefile_settings.pagefiles }}" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_partition/aliases b/ansible_collections/community/windows/tests/integration/targets/win_partition/aliases new file mode 100644 index 000000000..4cd27b3cb --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_partition/aliases @@ -0,0 +1 @@ +shippable/windows/group1 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_partition/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_partition/defaults/main.yml new file mode 100644 index 000000000..ca221d09b --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_partition/defaults/main.yml @@ -0,0 +1 @@ +AnsibleVhdx: '{{ remote_tmp_dir }}\AnsiblePart.vhdx' diff --git a/ansible_collections/community/windows/tests/integration/targets/win_partition/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_partition/meta/main.yml new file mode 100644 index 000000000..45806c8dc --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_partition/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_remote_tmp_dir
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_partition/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_partition/tasks/main.yml new file mode 100644 index 000000000..50d086ae1 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_partition/tasks/main.yml @@ -0,0 +1,18 @@ +--- +- name: Copy VHDX scripts + ansible.windows.win_template: + src: "{{ item.src }}" + dest: '{{ remote_tmp_dir }}\{{ item.dest }}' + loop: + - { src: vhdx_creation_script.j2, dest: vhdx_creation_script.txt } + - { src: vhdx_deletion_script.j2, dest: vhdx_deletion_script.txt } + +- name: Create VHD + ansible.windows.win_command: diskpart.exe /s "{{ remote_tmp_dir }}\vhdx_creation_script.txt" + +- name: Run tests + block: + - include: tests.yml + always: + - name: Detach disk + ansible.windows.win_command: diskpart.exe /s "{{ remote_tmp_dir }}\vhdx_deletion_script.txt" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_partition/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_partition/tasks/tests.yml new file mode 100644 index 000000000..83f189a6b --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_partition/tasks/tests.yml @@ -0,0 +1,261 @@ +--- +- name: Since partition is not present, disk_number is required to create a new partition. + win_partition: + drive_letter: D + register: incorrect_attempt_1 + ignore_errors: True + +- assert: + that: + - incorrect_attempt_1 is failed + - '"Missing required parameter: disk_number" in incorrect_attempt_1.msg' + +- name: Added disk_number but size is still absent + win_partition: + drive_letter: D + disk_number: 0 + register: incorrect_attempt_2 + ignore_errors: True + +- assert: + that: + - incorrect_attempt_2 is failed + - '"Missing required parameter: partition_size" in incorrect_attempt_2.msg' + +- name: Added size but the disk we specified earlier doesn't have enough space + win_partition: + drive_letter: D + disk_number: 1 + partition_size: 20 GiB + register: incorrect_attempt_3 + ignore_errors: True + +- assert: + that: + - incorrect_attempt_3 is failed + - '"Partition size is not supported by disk" in incorrect_attempt_3.msg' + +- name: Create 1 gib partition using drive_letter and default (huge) mbr type (check mode) + win_partition: + drive_letter: D + state: present + partition_size: 1 GiB + disk_number: 1 + active: True + register: create_small_part_check + check_mode: True + +- name: Create 1 gib partition using drive_letter and default (huge) mbr type + win_partition: + drive_letter: D + state: present + partition_size: 1 GiB + disk_number: 1 + active: True + register: create_small_part + +- name: Create 1 gib partition using drive_letter and default (huge) mbr type (idempotence) + win_partition: + drive_letter: D + state: present + partition_size: 1 GiB + disk_number: 1 + active: True + register: create_small_part_idempotence + +- name: "Check if partition was created successfully" + ansible.windows.win_shell: $AnsiPart = Get-Partition -DriveLetter D; "$($AnsiPart.DriveLetter),$($AnsiPart.Size),$($AnsiPart.IsActive),$($AnsiPart.MbrType)" + register: get_small_part + +- assert: + that: + - create_small_part_check is changed + - create_small_part is changed + - create_small_part_idempotence is not changed + - get_small_part.stdout | trim == "D,1073741824,True,6" + +- name: "Change drive letter, maximize partition size and set partition to read only (check mode)" + win_partition: + drive_letter: E + state: present + partition_size: -1 + disk_number: 1 + partition_number: 1 + read_only: True + register: upgrade_small_part_check + check_mode: True + +- name: "Change drive letter, maximize partition size and set partition to read only" + win_partition: + drive_letter: E + state: present + partition_size: -1 + disk_number: 1 + partition_number: 1 + read_only: True + register: upgrade_small_part + +- name: "Change drive letter, maximize partition size and set partition to read only (idempotence)" + win_partition: + drive_letter: E + state: present + partition_size: -1 + disk_number: 1 + partition_number: 1 + read_only: True + register: upgrade_small_part_idempotence + +- ansible.windows.win_shell: $AnsiPart = Get-Partition -DriveLetter E; "$($AnsiPart.DriveLetter),$($AnsiPart.Size),$($AnsiPart.IsReadOnly)" + register: get_max_part + +- name: Check if creation and updation were successful + assert: + that: + - upgrade_small_part_check is changed + - upgrade_small_part is changed + - upgrade_small_part_idempotence is not changed + - get_max_part.stdout | trim == "E,2096037888,True" + +- name: "Changing size of a read only partition" + win_partition: + drive_letter: E + partition_size: 1 GiB + register: modify_read_only_partition + ignore_errors: True + +- assert: + that: + - modify_read_only_partition is failed + +- name: "Delete partition (check mode)" + win_partition: + disk_number: 1 + partition_number: 1 + state: absent + register: delete_partition_check + check_mode: True + +- name: "Delete partition" + win_partition: + disk_number: 1 + partition_number: 1 + state: absent + register: delete_partition + +- name: "Delete partition (idempotence)" + win_partition: + disk_number: 1 + partition_number: 1 + state: absent + register: delete_partition_idempotence + +- name: "Confirm that the partition is absent" + ansible.windows.win_shell: Get-Partition -DiskNumber 1 -PartitionNumber 1 + register: confirm_partition_deletion + ignore_errors: True + +- assert: + that: + - delete_partition_check is changed + - delete_partition is changed + - delete_partition_idempotence is not changed + - '"No matching MSFT_Partition objects found" in confirm_partition_deletion.stderr' + +- name: "Create new partition without drive letter and ifs mbr type (check mode)" + win_partition: + disk_number: 1 + partition_size: -1 + mbr_type: ifs + offline: True + register: recreate_partition_check + check_mode: True + +- name: "Create new partition without drive letter and ifs mbr type" + win_partition: + disk_number: 1 + partition_size: -1 + mbr_type: ifs + offline: True + register: recreate_partition + +- name: "Create new partition without drive letter and ifs mbr type (idempotence failure)" # Disk is full now; no idempotence without drive letters + win_partition: + disk_number: 1 + partition_size: -1 + mbr_type: ifs + offline: True + register: recreate_partition_idempotence_failure + ignore_errors: True + +- name: "Confirm that new partition is created with maximum size, is offline and is IFS" + ansible.windows.win_shell: $AnsiPart = Get-Partition -DiskNumber 1 -PartitionNumber 1; "$($AnsiPart.Size),$($AnsiPart.IsOffline),$($AnsiPart.MbrType)" + register: confirm_recreate_partition + +- assert: + that: + - recreate_partition_check is changed + - recreate_partition is changed + - recreate_partition_idempotence_failure is failed + - confirm_recreate_partition.stdout | trim == "2096037888,True,7" + +- name: "Adding a drive letter to our partition should bring it back online (check mode)" + win_partition: + drive_letter: D + disk_number: 1 + partition_number: 1 + register: add_drive_letter_check + ignore_errors: True + check_mode: True + +- name: "Adding a drive letter to our partition should bring it back online" + win_partition: + drive_letter: D + disk_number: 1 + partition_number: 1 + register: add_drive_letter + ignore_errors: True + +- name: "Adding a drive letter to our partition should bring it back online (idempotence)" + win_partition: + drive_letter: D + disk_number: 1 + partition_number: 1 + register: add_drive_letter_idempotence + ignore_errors: True + +- name: "Confirm that drive is back online" + ansible.windows.win_shell: $AnsiPart = Get-Partition -DiskNumber 1 -PartitionNumber 1; "$($AnsiPart.DriveLetter),$($AnsiPart.IsOffline)" + register: confirm_add_drive_letter + ignore_errors: True + +- assert: + that: + - add_drive_letter_check is changed + - add_drive_letter is changed + - add_drive_letter_idempotence is not changed + - confirm_add_drive_letter.stdout | trim == "D,False" + +- name: "Remove partition again (check mode)" + win_partition: + drive_letter: D + state: absent + register: delete_partition_again_check + check_mode: True + +- name: "Remove partition again" + win_partition: + drive_letter: D + state: absent + register: delete_partition_again + +- name: "Remove partition again (idempotence)" + win_partition: + drive_letter: D + state: absent + register: delete_partition_again_idempotence + +- assert: + that: + - delete_partition_again_check is changed + - delete_partition_again is changed + - delete_partition_again_idempotence is not changed diff --git a/ansible_collections/community/windows/tests/integration/targets/win_partition/templates/vhdx_creation_script.j2 b/ansible_collections/community/windows/tests/integration/targets/win_partition/templates/vhdx_creation_script.j2 new file mode 100644 index 000000000..905373be1 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_partition/templates/vhdx_creation_script.j2 @@ -0,0 +1,7 @@ +create vdisk file="{{ AnsibleVhdx }}" maximum=2000 type=fixed + +select vdisk file="{{ AnsibleVhdx }}" + +attach vdisk + +convert mbr diff --git a/ansible_collections/community/windows/tests/integration/targets/win_partition/templates/vhdx_deletion_script.j2 b/ansible_collections/community/windows/tests/integration/targets/win_partition/templates/vhdx_deletion_script.j2 new file mode 100644 index 000000000..c2be9cd14 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_partition/templates/vhdx_deletion_script.j2 @@ -0,0 +1,3 @@ +select vdisk file="{{ AnsibleVhdx }}" + +detach vdisk diff --git a/ansible_collections/community/windows/tests/integration/targets/win_pester/aliases b/ansible_collections/community/windows/tests/integration/targets/win_pester/aliases new file mode 100644 index 000000000..423ce3910 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_pester/aliases @@ -0,0 +1 @@ +shippable/windows/group2 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_pester/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_pester/defaults/main.yml new file mode 100644 index 000000000..3507a0fda --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_pester/defaults/main.yml @@ -0,0 +1,3 @@ +--- +test_win_pester_path: C:\ansible\win_pester +test_report_file: c:\ansible\win_pester\test_report.xml diff --git a/ansible_collections/community/windows/tests/integration/targets/win_pester/files/fail.ps1 b/ansible_collections/community/windows/tests/integration/targets/win_pester/files/fail.ps1 new file mode 100644 index 000000000..4bd20a601 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_pester/files/fail.ps1 @@ -0,0 +1,2 @@ +# This makes sure that a file that does not end with *.test.ps1 does not run +throw "should never fail" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_pester/files/test01.tests.ps1 b/ansible_collections/community/windows/tests/integration/targets/win_pester/files/test01.tests.ps1 new file mode 100644 index 000000000..6248b2f77 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_pester/files/test01.tests.ps1 @@ -0,0 +1,5 @@ +Describe -Name 'Test01' { + It -name 'First Test' { + { Get-Service } | Should Not Throw + } +}
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_pester/files/test02.tests.ps1 b/ansible_collections/community/windows/tests/integration/targets/win_pester/files/test02.tests.ps1 new file mode 100644 index 000000000..6d2477cc8 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_pester/files/test02.tests.ps1 @@ -0,0 +1,5 @@ +Describe -Name 'Test02' { + It -name 'Second Test' { + { Get-Service } | Should Throw + } +}
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_pester/files/test03.tests.ps1 b/ansible_collections/community/windows/tests/integration/targets/win_pester/files/test03.tests.ps1 new file mode 100644 index 000000000..8dafbc6ff --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_pester/files/test03.tests.ps1 @@ -0,0 +1,11 @@ +Describe -Name 'Test03 without tag' { + It -name 'Third Test without tag' { + { Get-Service } | Should Not Throw + } +} + +Describe -Name 'Test03 with tag' -Tag tag1 { + It -name 'Third Test with tag' { + { Get-Service } | Should Not Throw + } +}
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_pester/files/test04.tests.ps1 b/ansible_collections/community/windows/tests/integration/targets/win_pester/files/test04.tests.ps1 new file mode 100644 index 000000000..07d1bd43c --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_pester/files/test04.tests.ps1 @@ -0,0 +1,18 @@ +Param( + $Service, + $Process +) + +Describe "Process should exist" { + it "Process $Process should be running" -Skip:([String]::IsNullOrEmpty($Process)) { + Get-Process -Name $Process -ErrorAction SilentlyContinue | Should Not BeNullOrEmpty + } +} + + + +Describe "Service should exist" -tag Service { + it "Service $Service should exist" -Skip:([String]::IsNullOrEmpty($Service)) { + Get-Service -Name $Service -ErrorAction SilentlyContinue | Should Not BeNullOrEmpty + } +} diff --git a/ansible_collections/community/windows/tests/integration/targets/win_pester/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_pester/tasks/main.yml new file mode 100644 index 000000000..baf6b0c50 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_pester/tasks/main.yml @@ -0,0 +1,60 @@ +--- +- name: create test folder(s) + ansible.windows.win_file: + path: '{{test_win_pester_path}}\{{item}}' + state: directory + with_items: + - Modules + - Tests + +- name: download Pester module from S3 bucket + ansible.windows.win_get_url: + # this was downloaded straight off the Pester GitHub release page and uploaded to the S3 bucket + # https://github.com/pester/Pester/releases + url: 'https://ansible-ci-files.s3.amazonaws.com/test/integration/roles/test_win_pester/Pester-4.3.1.zip' + dest: '{{test_win_pester_path}}\Pester-4.3.1.zip' + register: download_res + until: download_res is successful + retries: 3 + delay: 5 + +- name: unzip Pester module + win_unzip: + src: '{{test_win_pester_path}}\Pester-4.3.1.zip' + dest: '{{test_win_pester_path}}\Modules' + +- name: rename extracted zip to match module name + ansible.windows.win_shell: Rename-Item -Path '{{test_win_pester_path}}\Modules\Pester-4.3.1' -NewName Pester + +- name: add custom Pester location to the PSModulePath + ansible.windows.win_path: + name: PSModulePath + scope: machine + state: present + elements: + - '{{test_win_pester_path}}\Modules' + +- name: copy test files + ansible.windows.win_copy: + src: files/ + dest: '{{test_win_pester_path}}\Tests' + +- block: + - name: run Pester tests + include_tasks: test.yml + vars: + test_path: '{{ test_win_pester_path }}\Tests' + + always: + - name: remove custom pester location on the PSModulePath + ansible.windows.win_path: + name: PSModulePath + scope: machine + state: absent + elements: + - '{{test_win_pester_path}}\Modules' + + - name: delete test folder + ansible.windows.win_file: + path: '{{test_win_pester_path}}' + state: absent diff --git a/ansible_collections/community/windows/tests/integration/targets/win_pester/tasks/test.yml b/ansible_collections/community/windows/tests/integration/targets/win_pester/tasks/test.yml new file mode 100644 index 000000000..cbcbd4cbe --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_pester/tasks/test.yml @@ -0,0 +1,134 @@ +--- +- name: Run Pester test(s) specifying a fake test file + win_pester: + path: '{{test_path}}\fakefile.ps1' + register: fake_file + failed_when: '"Cannot find file or directory: ''" + test_path + "\\fakefile.ps1'' as it does not exist" not in fake_file.msg' + +- name: Run Pester test(s) specifying a fake folder + win_pester: + path: '{{test_path }}\fakedir' + register: fake_folder + failed_when: '"Cannot find file or directory: ''" + test_path + "\\fakedir'' as it does not exist" not in fake_folder.msg' + +- name: Run Pester test(s) specifying a test file and a higher pester version + win_pester: + path: '{{test_path}}\test01.tests.ps1' + minimum_version: '6.0.0' + register: invalid_version + failed_when: '"Pester version is not greater or equal to 6.0.0" not in invalid_version.msg' + +- name: Run Pester test(s) specifying a test file + win_pester: + path: '{{test_path}}\test01.tests.ps1' + register: file_result + +- name: assert Run Pester test(s) specify a test file + assert: + that: + - file_result.changed + - not file_result.failed + - file_result.output.TotalCount == 1 + +- name: Run Pester test(s) specifying a test file and with a minimum mandatory Pester version + win_pester: + path: '{{test_path}}\test01.tests.ps1' + minimum_version: 3.0.0 + register: file_result_with_version + +- name: assert Run Pester test(s) specifying a test file and a minimum mandatory Pester version + assert: + that: + - file_result_with_version.changed + - not file_result_with_version.failed + - file_result_with_version.output.TotalCount == 1 + +- name: Run Pester test(s) located in a folder. Folder path end with '\' + win_pester: + path: '{{test_path}}\' + register: dir_with_ending_slash + +- name: assert Run Pester test(s) located in a folder. Folder path end with '\' + assert: + that: + - dir_with_ending_slash.changed + - not dir_with_ending_slash.failed + - dir_with_ending_slash.output.TotalCount == 6 + +- name: Run Pester test(s) located in a folder. Folder path does not end with '\' + win_pester: + path: '{{test_path}}' + register: dir_without_ending_slash + +- name: assert Run Pester test(s) located in a folder. Folder does not end with '\' + assert: + that: + - dir_without_ending_slash.changed + - not dir_without_ending_slash.failed + - dir_without_ending_slash.output.TotalCount == 6 + +- name: Run Pester test(s) located in a folder and with a minimum mandatory Pester version + win_pester: + path: '{{test_path}}' + minimum_version: 3.0.0 + register: dir_with_version + +- name: assert Run Pester test(s) located in a folder and with a minimum mandatory Pester version + assert: + that: + - dir_with_version.changed + - not dir_with_version.failed + - dir_with_version.output.TotalCount == 6 + +- name: Run Pester test(s) specifying a test file without specifying tag + win_pester: + path: '{{test_path}}\test03.tests.ps1' + register: test_no_tag + +- name: assert Run Pester test(s) specifying a test file and all tests executed + assert: + that: + - test_no_tag.changed + - test_no_tag.output.TotalCount == 2 + +- name: Run Pester test(s) specifying a test file with tag + win_pester: + path: '{{test_path}}\test03.tests.ps1' + tags: tag1 + register: test_with_tag + +- name: Run Pester test(s) specifying a test file and only test with sepecified tag executed + assert: + that: + - test_with_tag.changed + - test_with_tag.output.TotalCount == 1 + +- name: Run Pester test(s) specifying a test file with parameters + win_pester: + path: '{{test_path}}\test04.tests.ps1' + test_parameters: + Process: lsass + Service: bits + register: test_with_parameter + +- name: Run Pester test(s) specifying a test file with parameters + assert: + that: + - test_with_parameter.changed + - test_with_parameter.output.PassedCount == 2 + - test_with_parameter.output.TotalCount == 2 + +- name: Run Pester test(s) specifying a test file by generating test result xml + win_pester: + path: '{{test_path}}\test03.tests.ps1' + output_file: '{{test_report_file}}' + +- name: Checks if the output result file exists + ansible.windows.win_stat: + path: '{{test_report_file}}' + register: test_output_file + +- name: Checks if the output result file exists + assert: + that: + - test_output_file.stat.exists diff --git a/ansible_collections/community/windows/tests/integration/targets/win_power_plan/aliases b/ansible_collections/community/windows/tests/integration/targets/win_power_plan/aliases new file mode 100644 index 000000000..215e0b069 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_power_plan/aliases @@ -0,0 +1 @@ +shippable/windows/group4 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_power_plan/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_power_plan/tasks/main.yml new file mode 100644 index 000000000..cffc8447c --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_power_plan/tasks/main.yml @@ -0,0 +1,128 @@ +# I dislike this but 2008 doesn't support the Win32_PowerPlan WMI provider +- name: get current plan details + ansible.windows.win_shell: | + $plan_info = powercfg.exe /list + ($plan_info | Select-String -Pattern '\(([\w\s]*)\) \*$').Matches.Groups[1].Value + ($plan_info | Select-String -Pattern '\(([\w\s]*)\)$').Matches.Groups[1].Value + register: plan_info + +- set_fact: + original_plan: '{{ plan_info.stdout_lines[0] }}' + name: '{{ plan_info.stdout_lines[1] }}' + +- block: + #Test that plan detects change is needed, but doesn't actually apply change + - name: set power plan (check mode) + win_power_plan: + name: "{{ name }}" + register: set_plan_check + check_mode: yes + + - name: get result of set power plan (check mode) + ansible.windows.win_shell: (powercfg.exe /list | Select-String -Pattern '\({{ name }}\)').Line + register: set_plan_check_result + changed_when: False + + # verify that the powershell check is showing the plan as still inactive on the system + - name: assert setting plan (check mode) + assert: + that: + - set_plan_check is changed + - not set_plan_check_result.stdout_lines[0].endswith('*') + + #Test that setting plan and that change is applied + - name: set power plan + win_power_plan: + name: "{{ name }}" + register: set_plan + + - name: get result of set power plan + ansible.windows.win_shell: (powercfg.exe /list | Select-String -Pattern '\({{ name }}\)').Line + register: set_plan_result + changed_when: False + + - name: assert setting plan + assert: + that: + - set_plan is changed + - set_plan_result.stdout_lines[0].endswith('*') + + #Test that plan doesn't apply change if it is already set + - name: set power plan (idempotent) + win_power_plan: + name: "{{ name }}" + register: set_plan_idempotent + + - name: assert setting plan (idempotent) + assert: + that: + - set_plan_idempotent is not changed + + always: + - name: always change back plan to the original when done testing + win_power_plan: + name: '{{ original_plan }}' + +- name: get current plan guid details + ansible.windows.win_shell: | + $plan_info = powercfg.exe /list + ($plan_info | Select-String -Pattern '([a-z0-9]+\-[a-z0-9]+\-[a-z0-9]+\-[a-z0-9]+\-[a-z0-9]+).+\*').Matches.Groups[1].Value + ($plan_info | Select-String -Pattern '([a-z0-9]+\-[a-z0-9]+\-[a-z0-9]+\-[a-z0-9]+\-[a-z0-9]+)').Matches.Groups[1].Value + register: guid_plan_info + +- set_fact: + original_plan: '{{ guid_plan_info.stdout_lines[0] }}' + name: '{{ guid_plan_info.stdout_lines[1] }}' + +- block: + #Test that plan detects change is needed, but doesn't actually apply change + - name: set power plan guid (check mode) + win_power_plan: + guid: "{{ name }}" + register: set_plan_check_guid + check_mode: yes + + - name: get result of set power plan guid (check mode) + ansible.windows.win_shell: (powercfg.exe /list | Select-String -Pattern '{{ name }}').Line + register: set_plan_check_result_guid + changed_when: False + + # verify that the powershell check is showing the plan as still inactive on the system + - name: assert setting plan guid (check mode) + assert: + that: + - set_plan_check_guid is changed + - not set_plan_check_result_guid.stdout_lines[0].endswith('*') + + #Test that setting plan and that change is applied + - name: set power plan guid + win_power_plan: + guid: "{{ name }}" + register: set_plan_guid + + - name: get result of set power plan guid + ansible.windows.win_shell: (powercfg.exe /list | Select-String -Pattern '{{ name }}').Line + register: set_plan_result_guid + changed_when: False + + - name: assert setting plan guid + assert: + that: + - set_plan_guid is changed + - set_plan_result_guid.stdout_lines[0].endswith('*') + + #Test that plan doesn't apply change if it is already set + - name: set power plan guid (idempotent) + win_power_plan: + guid: "{{ name }}" + register: set_plan_idempotent_guid + + - name: assert setting plan guid (idempotent) + assert: + that: + - set_plan_idempotent_guid is not changed + + always: + - name: always change back plan to the original when done testing guid + win_power_plan: + guid: '{{ original_plan }}' diff --git a/ansible_collections/community/windows/tests/integration/targets/win_product_facts/aliases b/ansible_collections/community/windows/tests/integration/targets/win_product_facts/aliases new file mode 100644 index 000000000..4cd27b3cb --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_product_facts/aliases @@ -0,0 +1 @@ +shippable/windows/group1 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_product_facts/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_product_facts/tasks/main.yml new file mode 100644 index 000000000..b1427eeaa --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_product_facts/tasks/main.yml @@ -0,0 +1,11 @@ +# This file is part of Ansible + +# Copyright: (c) 2017, Dag Wieers (dagwieers) <dag@wieers.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +- win_product_facts: + +- assert: + that: + - ansible_os_product_id is defined + - ansible_os_product_key is defined diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psexec/aliases b/ansible_collections/community/windows/tests/integration/targets/win_psexec/aliases new file mode 100644 index 000000000..4f4664b68 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psexec/aliases @@ -0,0 +1 @@ +shippable/windows/group5 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psexec/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_psexec/meta/main.yml new file mode 100644 index 000000000..9f37e96cd --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psexec/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_remote_tmp_dir diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psexec/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_psexec/tasks/main.yml new file mode 100644 index 000000000..5f4be3ebf --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psexec/tasks/main.yml @@ -0,0 +1,102 @@ +# 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 + ansible.windows.win_file: + path: '{{ testing_dir }}' + state: directory + +- name: Download PsExec + ansible.windows.win_get_url: + url: https://ansible-ci-files.s3.amazonaws.com/test/integration/targets/win_psexec/PsExec.exe + dest: '{{ testing_dir }}\PsExec.exe' + register: download_res + until: download_res is successful + retries: 3 + delay: 5 + +- name: Get the existing PATH env var + ansible.windows.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 +#- ansible.windows.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\"" + +- name: Run command with password + win_psexec: + command: whoami.exe + username: fake_user + password: '{{ item }}' + executable: '{{ testing_dir }}\PsExec.exe' + ignore_errors: yes # The username/password isn't valid so it will fail + loop: + - Testing123 + - Testing"123 # This makes sure the escaped password for the cmd is also masked + register: psexec_pass + +- name: Test password not in psexec_command output + assert: + that: + - psexec_pass.results[0].psexec_command.endswith("-u fake_user -p *PASSWORD_REPLACED* -accepteula whoami.exe") + - psexec_pass.results[1].psexec_command.endswith("-u fake_user -p *PASSWORD_REPLACED* -accepteula whoami.exe") diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psmodule/aliases b/ansible_collections/community/windows/tests/integration/targets/win_psmodule/aliases new file mode 100644 index 000000000..4cd27b3cb --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psmodule/aliases @@ -0,0 +1 @@ +shippable/windows/group1 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psmodule/files/module/license.txt b/ansible_collections/community/windows/tests/integration/targets/win_psmodule/files/module/license.txt new file mode 100644 index 000000000..81f2d4566 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psmodule/files/module/license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Ansible + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psmodule/files/module/template.nuspec b/ansible_collections/community/windows/tests/integration/targets/win_psmodule/files/module/template.nuspec new file mode 100644 index 000000000..cb52fb42d --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psmodule/files/module/template.nuspec @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> + <metadata> + <id>--- NAME ---</id> + <version>--- VERSION ---</version> + <authors>Ansible</authors> + <owners>Ansible</owners> + <requireLicenseAcceptance>--- LICACC ---</requireLicenseAcceptance> + <licenseUrl>https://choosealicense.com/licenses/mit/</licenseUrl> + <description>Test for Ansible win_ps* modules</description> + <releaseNotes></releaseNotes> + <copyright>Copyright (c) 2019 Ansible, licensed under MIT.</copyright> + <tags>PowerShellGetFormatVersion_2.0 PSModule PSIncludes_Function PSFunction_--- FUNCTION --- PSCommand_--- FUNCTION ---</tags> + </metadata> +</package> diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psmodule/files/module/template.psd1 b/ansible_collections/community/windows/tests/integration/targets/win_psmodule/files/module/template.psd1 new file mode 100644 index 000000000..cd6709722 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psmodule/files/module/template.psd1 @@ -0,0 +1,17 @@ +@{ + RootModule = '--- NAME ---.psm1' + ModuleVersion = '--- VERSION ---' + GUID = '--- GUID ---' + Author = 'Ansible' + Copyright = 'Copyright (c) 2019 Ansible, licensed under MIT.' + Description = "Test for Ansible win_ps* modules" + PowerShellVersion = '3.0' + FunctionsToExport = @( + "--- FUNCTION ---" + ) + PrivateData = @{ + PSData = @{ +--- PS_DATA --- + } + } +} diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psmodule/files/module/template.psm1 b/ansible_collections/community/windows/tests/integration/targets/win_psmodule/files/module/template.psm1 new file mode 100644 index 000000000..ac38fb5ed --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psmodule/files/module/template.psm1 @@ -0,0 +1,10 @@ +Function --- FUNCTION --- { + return [PSCustomObject]@{ + Name = "--- NAME ---" + Version = "--- VERSION ---" + Repo = "--- REPO ---" + } +} + +Export-ModuleMember -Function --- FUNCTION --- + diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psmodule/files/openssl.conf b/ansible_collections/community/windows/tests/integration/targets/win_psmodule/files/openssl.conf new file mode 100644 index 000000000..2b5685b43 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psmodule/files/openssl.conf @@ -0,0 +1,9 @@ +distinguished_name = req_distinguished_name + +[req_distinguished_name] + +[req_sign] +subjectKeyIdentifier=hash +basicConstraints = CA:FALSE +keyUsage = digitalSignature +extendedKeyUsage = codeSigning diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psmodule/files/setup_certs.sh b/ansible_collections/community/windows/tests/integration/targets/win_psmodule/files/setup_certs.sh new file mode 100644 index 000000000..258567316 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psmodule/files/setup_certs.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +# Generate key used for CA cert +openssl genrsa -aes256 -out ca.key -passout pass:password 2048 + +# Generate CA certificate +openssl req -new -x509 -days 365 -key ca.key -out ca.pem -subj "/CN=Ansible Root" -passin pass:password + +# Generate key used for signing cert +openssl genrsa -aes256 -out sign.key -passout pass:password 2048 + +# Generate CSR for signing cert that includes CodeSiging extension +openssl req -new -key sign.key -out sign.csr -subj "/CN=Ansible Sign" -config openssl.conf -reqexts req_sign -passin pass:password + +# Generate signing certificate +openssl x509 -req -in sign.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out sign.pem -days 365 -extfile openssl.conf -extensions req_sign -passin pass:password + +# Create pfx that includes signing cert and cert with the pass 'password' +openssl pkcs12 -export -out sign.pfx -inkey sign.key -in sign.pem -passin pass:password -passout pass:password diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psmodule/files/setup_modules.ps1 b/ansible_collections/community/windows/tests/integration/targets/win_psmodule/files/setup_modules.ps1 new file mode 100644 index 000000000..48d55b574 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psmodule/files/setup_modules.ps1 @@ -0,0 +1,89 @@ +$ErrorActionPreference = "Stop" + +$template_path = $args[0] +$template_manifest = Join-Path -Path $template_path -ChildPath template.psd1 +$template_script = Join-Path -Path $template_path -ChildPath template.psm1 +$template_nuspec = Join-Path -Path $template_path -ChildPath template.nuspec +$template_license = Join-Path -Path $template_path -ChildPath license.txt +$nuget_exe = Join-Path -Path $template_path -ChildPath nuget.exe +$sign_cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList @( + (Join-Path -Path $template_path -ChildPath sign.pfx), + 'password', + # We need to use MachineKeySet so we can load the pfx without using become + # EphemeralKeySet would be better but it is only available starting with .NET 4.7.2 + [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet +) + +$packages = @( + @{ name = "ansible-test1"; version = "1.0.0"; repo = "PSRepo 1"; function = "Get-AnsibleTest1" } + @{ name = "ansible-test1"; version = "1.0.5"; repo = "PSRepo 1"; function = "Get-AnsibleTest1" } + @{ name = "ansible-test1"; version = "1.1.0"; repo = "PSRepo 1"; function = "Get-AnsibleTest1" } + @{ name = "ansible-test2"; version = "1.0.0"; repo = "PSRepo 1"; function = "Get-AnsibleTest2" } + @{ name = "ansible-test2"; version = "1.0.0"; repo = "PSRepo 2"; function = "Get-AnsibleTest2" } + @{ name = "ansible-test2"; version = "1.0.1"; repo = "PSRepo 1"; function = "Get-AnsibleTest2"; signed = $false } + @{ name = "ansible-test2"; version = "1.1.0"; prerelease = "beta1"; repo = "PSRepo 1"; function = "Get-AnsibleTest2" } + @{ name = "ansible-clobber"; version = "0.1.0"; repo = "PSRepo 1"; function = "Enable-PSTrace" } + @{ name = "ansible-licensed" ; version = "1.1.1"; repo = "PSRepo 1" ; require_license = $true; function = "Get-AnsibleLicensed" } +) + +foreach ($package in $packages) { + $tmp_dir = Join-Path -Path $template_path -ChildPath $package.name + if (Test-Path -Path $tmp_dir) { + Remove-Item -Path $tmp_dir -Force -Recurse + } + New-Item -Path $tmp_dir -ItemType Directory > $null + + Copy-Item -LiteralPath $template_license -Destination $tmp_dir -Force + + try { + $ps_data = @("LicenseUri = 'https://choosealicense.com/licenses/mit/'") + $nuget_version = $package.version + if ($package.ContainsKey("prerelease")) { + $ps_data += "Prerelease = '$($package.prerelease)'" + $nuget_version = "$($package.version)-$($package.prerelease)" + } + if ($package.ContainsKey("require_license")) { + $ps_data += "RequireLicenseAcceptance = `$$($package.require_license)" + } + + $manifest = [System.IO.File]::ReadAllText($template_manifest) + $manifest = $manifest.Replace('--- NAME ---', $package.name).Replace('--- VERSION ---', $package.version) + $manifest = $manifest.Replace('--- GUID ---', [Guid]::NewGuid()).Replace('--- FUNCTION ---', $package.function) + + $manifest = $manifest.Replace('--- PS_DATA ---', $ps_data -join "`n") + $manifest_path = Join-Path -Path $tmp_dir -ChildPath "$($package.name).psd1" + Set-Content -Path $manifest_path -Value $manifest + + $script = [System.IO.File]::ReadAllText($template_script) + $script = $script.Replace('--- NAME ---', $package.name).Replace('--- VERSION ---', $package.version) + $script = $script.Replace('--- REPO ---', $package.repo).Replace('--- FUNCTION ---', $package.function) + $script_path = Join-Path -Path $tmp_dir -ChildPath "$($package.name).psm1" + Set-Content -Path $script_path -Value $script + + $signed = if ($package.ContainsKey("signed")) { $package.signed } else { $true } + if ($signed) { + Set-AuthenticodeSignature -Certificate $sign_cert -LiteralPath $manifest_path > $null + Set-AuthenticodeSignature -Certificate $sign_cert -LiteralPath $script_path > $null + } + + # We should just be able to use Publish-Module but it fails when running over WinRM for older hosts and become + # does not fix this. It fails to respond to nuget.exe push errors when it canno find the .nupkg file. We will + # just manually do that ourselves. This also has the added benefit of being a lot quicker than Publish-Module + # which seems to take forever to publish the module. + $nuspec = [System.IO.File]::ReadAllText($template_nuspec) + $nuspec = $nuspec.Replace('--- NAME ---', $package.name).Replace('--- VERSION ---', $nuget_version) + $nuspec = $nuspec.Replace('--- FUNCTION ---', $package.function) + $nuspec = $nuspec.Replace('--- LICACC ---', ($package.require_license -as [bool]).ToString().ToLower()) + Set-Content -Path (Join-Path -Path $tmp_dir -ChildPath "$($package.name).nuspec") -Value $nuspec + + &$nuget_exe pack "$tmp_dir\$($package.name).nuspec" -outputdirectory $tmp_dir + + $repo_path = Join-Path -Path $template_path -ChildPath $package.repo + $nupkg_filename = "$($package.name).$($nuget_version).nupkg" + Copy-Item -Path (Join-Path -Path $tmp_dir -ChildPath $nupkg_filename) ` + -Destination (Join-Path -Path $repo_path -ChildPath $nupkg_filename) + } + finally { + Remove-Item -Path $tmp_dir -Force -Recurse + } +} diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psmodule/handlers/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_psmodule/handlers/main.yml new file mode 100644 index 000000000..13797f236 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psmodule/handlers/main.yml @@ -0,0 +1,34 @@ +--- +- name: re-add PSGallery repository + ansible.windows.win_shell: Register-PSRepository -Default -InstallationPolicy Untrusted + +- name: remove registered repos + win_psrepository: + name: '{{ item }}' + state: absent + loop: + - PSRepo 1 + - PSRepo 2 + +- name: remove CA cert from trusted root store + ansible.windows.win_certificate_store: + thumbprint: '{{ ca_cert_import.thumbprints[0] }}' + store_location: LocalMachine + store_name: Root + state: absent + +- name: remove signing key from trusted publisher store + ansible.windows.win_certificate_store: + thumbprint: '{{ sign_cert_import.thumbprints[0] }}' + store_location: LocalMachine + store_name: TrustedPublisher + state: absent + +- name: remove test packages + win_psmodule: + name: '{{ item }}' + state: absent + loop: + - ansible-test1 + - ansible-test2 + - ansible-clobber
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psmodule/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_psmodule/meta/main.yml new file mode 100644 index 000000000..f0920878a --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psmodule/meta/main.yml @@ -0,0 +1,3 @@ +dependencies: +- setup_remote_tmp_dir +- setup_win_psget diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psmodule/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_psmodule/tasks/main.yml new file mode 100644 index 000000000..ffe1a0978 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psmodule/tasks/main.yml @@ -0,0 +1,513 @@ +# test code for the win_psmodule module when using winrm connection +# Copyright: (c) 2018, Wojciech Sciesinski <wojciech[at]sciesinski[dot]net> +# Copyright: (c) 2017, Daniele Lazzari <lazzari@mailup.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: setup test repos and modules + import_tasks: setup.yml + +# Remove the below task in Ansible 2.12 +- name: ensure warning is fired when adding a repo + win_psmodule: + name: ansible-test1 + repository: some repo + url: '{{ remote_tmp_dir }}' + state: present + register: dep_repo_add + ignore_errors: yes # will fail because this repo doesn't actually have this module + check_mode: yes + +- name: assert warning is fired when adding a repo + assert: + that: + - dep_repo_add is changed + - dep_repo_add.deprecations|length == 1 + - dep_repo_add.deprecations[0].msg == 'Adding a repo with this module is deprecated, the repository parameter should only be used to select a repo. Use community.windows.win_psrepository to manage repos' + - dep_repo_add.deprecations[0].date == '2021-07-01' + +### licensed module checks +# it is not known in check mode that a module requires +# license acceptance, so we don't do check mode tests +# for that scenario + +- name: install module requiring license acceptance (no param) + win_psmodule: + name: ansible-licensed + state: present + register: install_licensed + ignore_errors: yes + +- name: get result of install package + ansible.windows.win_shell: (Get-Module -ListAvailable -Name ansible-licensed | Measure-Object).Count + register: install_actual_check + +- name: assert install package (failure) + assert: + that: + - install_licensed is failed + - '"License Acceptance is required for module" in install_licensed.msg' + - install_actual_check.stdout | trim | int == 0 + +- name: install module requiring license acceptance + win_psmodule: + name: ansible-licensed + state: present + accept_license: true + register: install_licensed + +- name: get result of install package + ansible.windows.win_shell: Import-Module -Name ansible-licensed; Get-AnsibleLicensed | ConvertTo-Json + register: install_actual + +- name: assert install package + assert: + that: + - install_licensed is changed + - install_actual.stdout | from_json == {"Name":"ansible-licensed","Version":"1.1.1","Repo":"PSRepo 1"} + +- name: install module requiring license acceptance (idempotence) + win_psmodule: + name: ansible-licensed + state: present + accept_license: true + register: install_licensed + +- name: get result of install package (idempotence) + ansible.windows.win_shell: Import-Module -Name ansible-licensed; Get-AnsibleLicensed | ConvertTo-Json + register: install_actual + +- name: assert install package (idempotence) + assert: + that: + - install_licensed is not changed + - install_actual.stdout | from_json == {"Name":"ansible-licensed","Version":"1.1.1","Repo":"PSRepo 1"} + +### end licensed module checks + +- name: install package (check mode) + win_psmodule: + name: ansible-test1 + state: present + register: install_check + check_mode: yes + +- name: get result of install package (check mode) + ansible.windows.win_shell: (Get-Module -ListAvailable -Name ansible-test1 | Measure-Object).Count + register: install_actual_check + +- name: assert install package (check mode) + assert: + that: + - install_check is changed + - install_actual_check.stdout | trim | int == 0 + +- name: install package + win_psmodule: + name: ansible-test1 + state: present + register: install + +- name: get result of install package + ansible.windows.win_shell: Import-Module -Name ansible-test1; Get-AnsibleTest1 | ConvertTo-Json + register: install_actual + +- name: assert install package + assert: + that: + - install is changed + - install_actual.stdout | from_json == {"Name":"ansible-test1","Version":"1.1.0","Repo":"PSRepo 1"} + +- name: install package (idempotent) + win_psmodule: + name: ansible-test1 + state: present + register: install_again + +- name: assert install package (idempotent) + assert: + that: + - not install_again is changed + +- name: remove package (check mode) + win_psmodule: + name: ansible-test1 + state: absent + register: remove_check + check_mode: yes + +- name: get result of remove package (check mode) + ansible.windows.win_shell: (Get-Module -ListAvailable -Name ansible-test1 | Measure-Object).Count + register: remove_actual_check + +- name: remove package (check mode) + assert: + that: + - remove_check is changed + - remove_actual_check.stdout | trim | int == 1 + +- name: remove package + win_psmodule: + name: ansible-test1 + state: absent + register: remove + +- name: get result of remove package + ansible.windows.win_shell: (Get-Module -ListAvailable -Name ansible-test1 | Measure-Object).Count + register: remove_actual + +- name: assert remove package + assert: + that: + - remove is changed + - remove_actual.stdout | trim | int == 0 + +- name: remove package (idempotent) + win_psmodule: + name: ansible-test1 + state: absent + register: remove_again + +- name: assert remove package (idempotent) + assert: + that: + - not remove_again is changed + +- name: fail to install module that exists in multiple repos + win_psmodule: + name: ansible-test2 + state: present + register: fail_multiple_pkg + failed_when: 'fail_multiple_pkg.msg != "Problems installing ansible-test2 module: Unable to install, multiple modules matched ''ansible-test2''. Please specify a single -Repository."' + +- name: install module with specific repository + win_psmodule: + name: ansible-test2 + repository: PSRepo 2 + state: present + register: install_repo + +- name: get result of install module with specific repository + ansible.windows.win_shell: Import-Module -Name ansible-test2; Get-AnsibleTest2 | ConvertTo-Json + register: install_repo_actual + +- name: assert install module with specific repository + assert: + that: + - install_repo is changed + - install_repo_actual.stdout | from_json == {"Name":"ansible-test2","Version":"1.0.0","Repo":"PSRepo 2"} + +- name: install module with specific repository (idempotent) + win_psmodule: + name: ansible-test2 + repository: PSRepo 2 + state: present + register: install_repo_again + +- name: assert install module with specific repository (idempotent) + assert: + that: + - not install_repo_again is changed + +- name: remove package that was installed from specific repository + win_psmodule: + name: ansible-test2 + state: absent + register: remove_repo_without_source + +- name: get result of remove package that was installed from specific repository + ansible.windows.win_shell: (Get-Module -ListAvailable -Name ansible-test2 | Measure-Object).Count + register: remove_repo_without_source_actual + +- name: assert remove package that was installed from specific repository + assert: + that: + - remove_repo_without_source is changed + - remove_repo_without_source_actual.stdout | trim | int == 0 + +- name: fail to install required version that is missing + win_psmodule: + name: ansible-test1 + required_version: 0.9.0 + state: present + register: fail_missing_req + failed_when: '"Problems installing ansible-test1 module: No match was found for the specified search criteria" not in fail_missing_req.msg' + +- name: install required version + win_psmodule: + name: ansible-test1 + required_version: 1.0.0 + state: present + register: install_req_version + +- name: get result of install required version + ansible.windows.win_shell: Import-Module -Name ansible-test1; Get-AnsibleTest1 | ConvertTo-Json + register: install_req_version_actual + +- name: assert install required version + assert: + that: + - install_req_version is changed + - install_req_version_actual.stdout | from_json == {"Name":"ansible-test1","Version":"1.0.0","Repo":"PSRepo 1"} + +- name: install required version (idempotent) + win_psmodule: + name: ansible-test1 + required_version: 1.0.0 + state: present + register: install_req_version_again + +- name: assert install required version (idempotent) + assert: + that: + - not install_req_version_again is changed + +- name: remove required version + win_psmodule: + name: ansible-test1 + required_version: 1.0.0 + state: absent + register: remove_req_version + +- name: get result of remove required version + ansible.windows.win_shell: (Get-Module -ListAvailable -Name ansible-test1 | Measure-Object).Count + register: remove_req_version_actual + +- name: assert remove required version + assert: + that: + - remove_req_version is changed + - remove_req_version_actual.stdout | trim | int == 0 + +- name: remove required version (idempotent) + win_psmodule: + name: ansible-test1 + required_version: 1.0.0 + state: absent + register: remove_req_version_again + +- name: assert remove required version (idempotent) + assert: + that: + - not remove_req_version_again is changed + +- name: install min max version + win_psmodule: + name: ansible-test1 + minimum_version: 1.0.1 + maximum_version: 1.0.9 + state: present + register: install_min_max + +- name: get result of install min max version + ansible.windows.win_shell: Import-Module -Name ansible-test1; Get-AnsibleTest1 | ConvertTo-Json + register: install_min_max_actual + +- name: assert install min max version + assert: + that: + - install_min_max is changed + - install_min_max_actual.stdout | from_json == {"Name":"ansible-test1","Version":"1.0.5","Repo":"PSRepo 1"} + +- name: install min max version (idempotent) + win_psmodule: + name: ansible-test1 + minimum_version: 1.0.1 + maximum_version: 1.0.9 + state: present + register: install_min_max_again + +- name: assert install min max version (idempotent) + assert: + that: + - not install_min_max_again is changed + +- name: update package to latest version + win_psmodule: + name: ansible-test1 + state: latest + register: update_module + +- name: get result of update package to latest version + ansible.windows.win_shell: Import-Module -Name ansible-test1; Get-AnsibleTest1 | ConvertTo-Json + register: update_module_actual + +- name: assert update package to latest version + assert: + that: + - update_module is changed + - update_module_actual.stdout | from_json == {"Name":"ansible-test1","Version":"1.1.0","Repo":"PSRepo 1"} + +- name: update package to latest version (idempotent) + win_psmodule: + name: ansible-test1 + state: latest + register: update_module_again + +- name: assert update package to latest version (idempotent) + assert: + that: + - not update_module_again is changed + +- name: remove package that does not match min version + win_psmodule: + name: ansible-test1 + minimum_version: 2.0.0 + state: absent + register: remove_min_no_change + +- name: assert remove package that does not match min version + assert: + that: + - not remove_min_no_change is changed + +- name: remove package that does not match max version + win_psmodule: + name: ansible-test1 + maximum_version: 0.9.0 + state: absent + register: remove_max_no_change + +- name: assert remove package that does not match max version + assert: + that: + - not remove_max_no_change is changed + +- name: uninstall package to clear tests + win_psmodule: + name: ansible-test1 + state: absent + +- name: install package with max version + win_psmodule: + name: ansible-test2 + maximum_version: 1.0.0 + repository: PSRepo 1 + state: present + register: install_max + +- name: get result of install package with max version + ansible.windows.win_shell: Import-Module -Name ansible-test2; Get-AnsibleTest2 | ConvertTo-Json + register: install_max_actual + +- name: assert install package with max version + assert: + that: + - install_max is changed + - install_max_actual.stdout | from_json == {"Name":"ansible-test2","Version":"1.0.0","Repo":"PSRepo 1"} + +- name: fail to install updated package without skip publisher + win_psmodule: + name: ansible-test2 + required_version: 1.0.1 # This version has been pureposefully not been signed for testing + repository: PSRepo 1 + state: present + register: fail_skip_pub + failed_when: '"The version ''1.0.1'' of the module ''ansible-test2'' being installed is not catalog signed" not in fail_skip_pub.msg' + +- name: install updated package provider with skip publisher + win_psmodule: + name: ansible-test2 + required_version: 1.0.1 + repository: PSRepo 1 + state: present + skip_publisher_check: yes + register: install_skip_pub + +- name: get result of install updated package provider with skip publisher + ansible.windows.win_shell: Import-Module -Name ansible-test2; Get-AnsibleTest2 | ConvertTo-Json + register: install_skip_pub_actual + +- name: assert install updated package provider with skip publisher + assert: + that: + - install_skip_pub is changed + - install_skip_pub_actual.stdout | from_json == {"Name":"ansible-test2","Version":"1.0.1","Repo":"PSRepo 1"} + +- name: remove test package 2 for clean test + win_psmodule: + name: ansible-test2 + state: absent + +- name: fail to install clobbered module + win_psmodule: + name: ansible-clobber + state: present + register: fail_clobbering_time + failed_when: '"If you still want to install this module ''ansible-clobber'', use -AllowClobber parameter." not in fail_clobbering_time.msg' + +- name: install clobbered module + win_psmodule: + name: ansible-clobber + allow_clobber: yes + state: present + register: install_clobber + +- name: get result of install clobbered module + ansible.windows.win_shell: Import-Module -Name ansible-clobber; Enable-PSTrace | ConvertTo-Json + register: install_clobber_actual + +- name: assert install clobbered module + assert: + that: + - install_clobber is changed + - install_clobber_actual.stdout | from_json == {"Name":"ansible-clobber","Version":"0.1.0","Repo":"PSRepo 1"} + +- name: remove clobbered module + win_psmodule: + name: ansible-clobber + state: absent + register: remove_clobber + +- name: get result of remove clobbered module + ansible.windows.win_shell: (Get-Module -ListAvailable -Name ansible-clobber | Measure-Object).Count + register: remove_clobber_actual + +- name: assert remove clobbered module + assert: + that: + - remove_clobber is changed + - remove_clobber_actual.stdout | trim | int == 0 + +- name: fail to install prerelese module + win_psmodule: + name: ansible-test2 + repository: PSRepo 1 + required_version: 1.1.0-beta1 + state: present + register: fail_install_prerelease + failed_when: '"The ''-AllowPrerelease'' parameter must be specified when using the Prerelease string" not in fail_install_prerelease.msg' + +- name: install prerelease module + win_psmodule: + name: ansible-test2 + repository: PSRepo 1 + required_version: 1.1.0-beta1 + allow_prerelease: yes + state: present + register: install_prerelease + +- name: get result of install prerelease module + ansible.windows.win_shell: Import-Module -Name ansible-test2; Get-AnsibleTest2 | ConvertTo-Json + register: install_prerelease_actual + +- name: assert install prerelease module + assert: + that: + - install_prerelease is changed + - install_prerelease_actual.stdout | from_json == {"Name":"ansible-test2","Version":"1.1.0","Repo":"PSRepo 1"} + +- name: remove prerelease module + win_psmodule: + name: ansible-test2 + state: absent + register: remove_prerelease + +- name: get result of remove prerelease module + ansible.windows.win_shell: (Get-Module -ListAvailable -Name ansible-test2 | Measure-Object).Count + register: remove_prerelease_actual + +- name: assert remove prerelease module + assert: + that: + - remove_prerelease is changed + - remove_prerelease_actual.stdout | trim | int == 0 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psmodule/tasks/setup.yml b/ansible_collections/community/windows/tests/integration/targets/win_psmodule/tasks/setup.yml new file mode 100644 index 000000000..42049a3e3 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psmodule/tasks/setup.yml @@ -0,0 +1,119 @@ +# Sets up 2 local repos that contains mock packages for testing. +# +# PSRepo 1 contains +# ansible-test1 - 1.0.0 +# ansible-test1 - 1.0.5 +# ansible-test1 - 1.1.0 +# ansible-test2 - 1.0.0 +# ansible-test2 - 1.0.1 (Not signed for skip_publisher tests) +# ansible-test2 - 1.1.0-beta1 +# ansible-clobber - 0.1.0 +# ansible-licensed - 1.1.1 (requires license acceptance) +# PSRepo 2 contains +# ansible-test2 - 1.0.0 +# +# These modules will have the following cmdlets +# ansible-test1 +# Get-AnsibleTest1 +# +# ansible-test2 +# Get-AnsibleTest2 +# +# ansible-clobber +# Enable-PSTrace (clobbers the Enable-PSTrace cmdlet) +# +# All cmdlets return +# [PSCustomObject]@{ +# Name = "the name of the module" +# Version = "the version of the module" +# Repo = "the repo where the module was sourced from" +# } +--- +- name: create test repo folders + ansible.windows.win_file: + path: '{{ remote_tmp_dir }}\{{ item }}' + state: directory + loop: + - PSRepo 1 + - PSRepo 2 + +- name: register test repos + win_psrepository: + name: '{{ item.name }}' + source: '{{ remote_tmp_dir }}\{{ item.name }}' + installation_policy: '{{ item.policy }}' + notify: remove registered repos + loop: + - name: PSRepo 1 + policy: trusted + - name: PSRepo 2 + policy: untrusted + +- name: remove PSGallery repository + win_psrepository: + name: PSGallery + state: absent + notify: re-add PSGallery repository + +- name: create custom openssl conf + copy: + src: openssl.conf + dest: '{{ output_dir }}/openssl.conf' + delegate_to: localhost + +- name: get absolute path of output_dir for script + shell: echo {{ output_dir }} + delegate_to: localhost + register: output_dir_abs + +- name: create certificates for code signing + script: setup_certs.sh + args: + chdir: '{{ output_dir_abs.stdout }}' + delegate_to: localhost + +- name: copy the CA and sign certificates + ansible.windows.win_copy: + src: '{{ output_dir }}/{{ item }}' + dest: '{{ remote_tmp_dir }}\' + loop: + - ca.pem + - sign.pem + - sign.pfx + +- name: import the CA key to the trusted root store + ansible.windows.win_certificate_store: + path: '{{ remote_tmp_dir }}\ca.pem' + state: present + store_location: LocalMachine + store_name: Root + register: ca_cert_import + notify: remove CA cert from trusted root store + +- name: import the sign key to the trusted publisher store + ansible.windows.win_certificate_store: + path: '{{ remote_tmp_dir }}\sign.pem' + state: present + store_location: LocalMachine + store_name: TrustedPublisher + register: sign_cert_import + notify: remove signing key from trusted publisher store + +- name: copy across module template files + ansible.windows.win_copy: + src: module/ + dest: '{{ remote_tmp_dir }}' + +# Used in the script below to create the .nupkg for each test module +- name: download NuGet binary for module publishing + ansible.windows.win_get_url: + url: https://ansible-ci-files.s3.amazonaws.com/test/integration/targets/win_psmodule/nuget.exe + dest: '{{ remote_tmp_dir }}' + register: download_res + until: download_res is successful + retries: 3 + delay: 5 + +- name: create test PowerShell modules + script: setup_modules.ps1 "{{ remote_tmp_dir }}" + notify: remove test packages diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/aliases b/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/aliases new file mode 100644 index 000000000..423ce3910 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/aliases @@ -0,0 +1 @@ +shippable/windows/group2 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/defaults/main.yml new file mode 100644 index 000000000..9c9c9c078 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/defaults/main.yml @@ -0,0 +1,70 @@ +--- +run_check_mode: False +suffix: "{{ '(check mode)' if run_check_mode else '' }}" +repository_name: Repo +repo_path: "{{ remote_tmp_dir }}\\repo\\" + +modules_to_install: + - AnsibleVault + - PSCSharpInvoker + - PInvokeHelper + +builtin_modules: + - Microsoft.PowerShell.Utility + - Microsoft.PowerShell.Host + - Microsoft.PowerShell.Management + +sample_modules: "{{ builtin_modules + modules_to_install }}" + +expected_fields: + - prefix + - private_data + - scripts + - author + - description + - exported_type_files + - exported_workflows + - nested_modules + - exported_dsc_resources + - updated_date + - root_module + - power_shell_host_name + - module_base + - path + - exported_format_files + - dependencies + - release_notes + - installed_date + - exported_variables + - published_date + - required_assemblies + - version + - icon_uri + - project_uri + - dot_net_framework_version + - processor_architecture + - license_uri + - required_modules + - module_type + - compatible_ps_editions + - company_name + - copyright + - tags + - access_mode + - package_management_provider + - exported_aliases + - power_shell_host_version + - clr_version + - file_list + - help_info_uri + - module_list + - exported_functions + - power_shell_version + - guid + - repository_source_location + - exported_cmdlets + - exported_commands + - repository + - installed_location + - name + - log_pipeline_execution_details diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/files/ansiblevault.0.3.0.nupkg b/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/files/ansiblevault.0.3.0.nupkg Binary files differnew file mode 100644 index 000000000..52fe289ed --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/files/ansiblevault.0.3.0.nupkg diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/files/pinvokehelper.0.1.0.nupkg b/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/files/pinvokehelper.0.1.0.nupkg Binary files differnew file mode 100644 index 000000000..05d30e55c --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/files/pinvokehelper.0.1.0.nupkg diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/files/pscsharpinvoker.0.1.0.nupkg b/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/files/pscsharpinvoker.0.1.0.nupkg Binary files differnew file mode 100644 index 000000000..77bd15f0e --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/files/pscsharpinvoker.0.1.0.nupkg diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/meta/main.yml new file mode 100644 index 000000000..acc7fbcae --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/meta/main.yml @@ -0,0 +1,3 @@ +dependencies: + - setup_remote_tmp_dir + - setup_win_psget diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/tasks/common.yml b/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/tasks/common.yml new file mode 100644 index 000000000..290f9efbd --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/tasks/common.yml @@ -0,0 +1,37 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: Assert that the correct structure is returned {{ suffix }} + assert: + that: + - module_info.modules is defined + - module_info.modules is sequence() + quiet: yes + +- name: Assert that the correct number of modules are returned {{ suffix }} + assert: + that: module_info.modules | length >= expected_modules | length + fail_msg: >- + Expected {{ expected_modules | length }} modules, got {{ module_info.modules | map(attribute='name') | join(',') }} ({{ module_info.modules | length}}) + quiet: yes + +- name: Assert that all expected modules are present {{ suffix }} + assert: + that: item in (module_info.modules | map(attribute='name')) + fail_msg: "Expected module '{{ item }}' not found in results." + quiet: yes + loop: "{{ expected_modules }}" + loop_control: + label: "Assert '{{ item }}' in result." + +- include_tasks: contains_all_fields.yml + vars: + dict_to_check: "{{ item }}" + loop: "{{ + only_check_first + | default(True) + | bool + | ternary([ module_info.modules[0] ], module_info.modules) + }}" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/tasks/contains_all_fields.yml b/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/tasks/contains_all_fields.yml new file mode 100644 index 000000000..20750e003 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/tasks/contains_all_fields.yml @@ -0,0 +1,13 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: Check for key ('{{ key }}') in result + assert: + that: key in dict_to_check + quiet: yes + fail_msg: "'{{ key }}' not found in dict." + loop_control: + loop_var: key + loop: "{{ expected_fields }}" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/tasks/main.yml new file mode 100644 index 000000000..21ed04a21 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/tasks/main.yml @@ -0,0 +1,49 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: Reset repositories + ansible.windows.win_shell: | + Get-PSRepository | Unregister-PSRepository + Register-PSRepository -Default + +- name: Set up directory repository + ansible.windows.win_copy: + src: "{{ role_path }}/files/" + dest: "{{ repo_path }}" + force: no + +- name: Register repository + community.windows.win_psrepository: + name: "{{ repository_name }}" + source_location: "{{ repo_path }}" + installation_policy: trusted + +- block: + - name: Install Modules + community.windows.win_psmodule: + name: "{{ item }}" + state: latest + repository: "{{ repository_name }}" + loop: "{{ modules_to_install }}" + + - name: Run Tests + import_tasks: tests.yml + + - name: Run Tests (check mode) + import_tasks: tests.yml + vars: + run_check_mode: True + + always: + - name: Remove Modules + community.windows.win_psmodule: + name: "{{ item }}" + state: absent + loop: "{{ modules_to_install }}" + + - name: Reset repositories + ansible.windows.win_shell: | + Get-PSRepository | Unregister-PSRepository + Register-PSRepository -Default diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/tasks/tests.yml new file mode 100644 index 000000000..688c994d2 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psmodule_info/tasks/tests.yml @@ -0,0 +1,85 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: Get info on a named module + vars: + expected_modules: + - "{{ builtin_modules[0] }}" + block: + - name: Get single module {{ suffix }} + community.windows.win_psmodule_info: + name: "{{ expected_modules[0] }}" + register: module_info + + - include_tasks: common.yml + + # block + check_mode: "{{ run_check_mode }}" + +- name: Get info on modules by wildcard match + vars: + wildcard: "Microsoft.PowerShell.*" + expected_modules: "{{ builtin_modules }}" + block: + - name: Get multiple modules by wildcard {{ suffix }} + community.windows.win_psmodule_info: + name: "{{ wildcard }}" + register: module_info + + - include_tasks: common.yml + + # block + check_mode: "{{ run_check_mode }}" + +- name: Get info on modules by repository + vars: + repository: "{{ repository_name }}" + expected_modules: "{{ modules_to_install }}" + block: + - name: Get multiple modules by repository {{ suffix }} + community.windows.win_psmodule_info: + repository: "{{ repository }}" + register: module_info + + - include_tasks: common.yml + + # block + check_mode: "{{ run_check_mode }}" + +- name: Get info on modules by repository and name pattern + vars: + wildcard: "*t" + repository: "{{ repository_name }}" + expected_modules: + - "AnsibleVault" + block: + - name: Get multiple modules by wildcard and repository {{ suffix }} + community.windows.win_psmodule_info: + name: "{{ wildcard }}" + repository: "{{ repository }}" + register: module_info + + - include_tasks: common.yml + + # block + check_mode: "{{ run_check_mode }}" + +- name: Get info on all modules + vars: + expected_modules: "{{ sample_modules }}" + block: + - name: Get all modules {{ suffix }} + community.windows.win_psmodule_info: + register: module_info + + - include_tasks: common.yml + + # one last time checking all the fields + - include_tasks: common.yml + vars: + only_check_first: False + + # block + check_mode: "{{ run_check_mode }}" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository/aliases b/ansible_collections/community/windows/tests/integration/targets/win_psrepository/aliases new file mode 100644 index 000000000..0d6b4f220 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository/aliases @@ -0,0 +1,2 @@ +shippable/windows/group4 +needs/httptester diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_psrepository/defaults/main.yml new file mode 100644 index 000000000..55e53a535 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository/defaults/main.yml @@ -0,0 +1,25 @@ +--- +repository_name: My Get +repository_source_location: https://www.nuget.org/api/v2 +repository_publish_location: '{{ repository_source_location }}/fake/publish' +repository_script_source_location: '{{ repository_source_location }}/fake/script' +repository_script_publish_location: '{{ repository_source_location }}/fake/script/publish' + +repository_source_location2: '{{ remote_tmp_dir }}' + +redirect_url: http://{{ httpbin_host }}/redirect +redirect_expected_target: http://{{ httpbin_host }}/get + +redirect_publish_url: http://{{ httpbin_host }}/redirect-to?url=http%3A%2F%2F{{ httpbin_host }}%2Fcache&status_code=307 +redirect_publish_expected_target: http://{{ httpbin_host }}/cache + +redirect_script_source_url: http://{{ httpbin_host }}/redirect-to?url=http%3A%2F%2F{{ httpbin_host }}%2Fxml&status_code=301 +redirect_script_source_expected_target: http://{{ httpbin_host }}/xml + +redirect_script_publish_url: http://{{ httpbin_host }}/redirect-to?url=http%3A%2F%2F{{ httpbin_host }}%2Fjson&status_code=302 +redirect_script_publish_expected_target: http://{{ httpbin_host }}/json + +repository_source_location_alt: http://{{ httpbin_host }}/response-headers +repository_publish_location_alt: http://{{ httpbin_host }}/gzip +repository_script_source_location_alt: http://{{ httpbin_host }}/html +repository_script_publish_location_alt: http://{{ httpbin_host }}/deflate diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_psrepository/meta/main.yml new file mode 100644 index 000000000..9e7a71888 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository/meta/main.yml @@ -0,0 +1,4 @@ +dependencies: +- setup_http_tests +- setup_remote_tmp_dir +- setup_win_psget diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository/tasks/get_repo_info.yml b/ansible_collections/community/windows/tests/integration/targets/win_psrepository/tasks/get_repo_info.yml new file mode 100644 index 000000000..999febcc6 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository/tasks/get_repo_info.yml @@ -0,0 +1,17 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: Retrieve Repository Manually + ansible.windows.win_shell: | + $repo = Get-PSRepository -Name {{ repository_name | quote }} -ErrorAction SilentlyContinue + @{ + repo = $repo + exists = $repo -as [bool] + } | ConvertTo-Json -Depth 5 + register: _retrieve_repo_result + +- name: Set Repo Response Variable + set_fact: + "{{ repo_result_var | default('repo_result') }}": "{{ _retrieve_repo_result.stdout | from_json }}" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_psrepository/tasks/main.yml new file mode 100644 index 000000000..0afdfbd2a --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository/tasks/main.yml @@ -0,0 +1,18 @@ +# This file is part of Ansible + +# Copyright: (c) 2018, Wojciech Sciesinski <wojciech[at]sciesinski[dot]net> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +--- +- name: unregister the repository + ansible.windows.win_shell: Unregister-PSRepository {{ repository_name | quote }} -ErrorAction SilentlyContinue + +- block: + - name: run all tests + include_tasks: tests.yml + + - name: run update and force behavior tests + include_tasks: update_and_force.yml + always: + - name: ensure test repo is unregistered + ansible.windows.win_shell: Unregister-PSRepository {{ repository_name | quote }} -ErrorAction SilentlyContinue diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_psrepository/tasks/tests.yml new file mode 100644 index 000000000..0fa63d9d0 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository/tasks/tests.yml @@ -0,0 +1,200 @@ +# This file is part of Ansible + +# Copyright: (c) 2018, Wojciech Sciesinski <wojciech[at]sciesinski[dot]net> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +--- + +- name: check adding of repository defaults - check mode + win_psrepository: + name: "{{ repository_name }}" + source: "{{ repository_source_location }}" + state: present + check_mode: True + register: adding_repository_check + +- name: get result of adding repository defaults - check mode + ansible.windows.win_shell: (Get-PSRepository -Name {{ repository_name | quote }} -ErrorAction SilentlyContinue | Measure-Object).Count + changed_when: false + register: result_adding_repository_check + +- name: test adding repository defaults - check mode + assert: + that: + - adding_repository_check is changed + - result_adding_repository_check.stdout_lines[0] == '0' + +- name: check adding repository defaults + win_psrepository: + name: "{{ repository_name }}" + source: "{{ repository_source_location }}" + state: present + register: adding_repository + +- name: get result of adding repository defaults + ansible.windows.win_shell: | + $repo = Get-PSRepository -Name {{ repository_name | quote }} + ($repo | Measure-Object).Count + $repo.SourceLocation + $repo.InstallationPolicy + register: result_adding_repository + +- name: test adding repository defaults + assert: + that: + - adding_repository is changed + - result_adding_repository.stdout_lines[0] == '1' + - result_adding_repository.stdout_lines[1] == repository_source_location + - result_adding_repository.stdout_lines[2] == 'Trusted' + +- name: check adding repository defaults - idempotent + win_psrepository: + name: "{{ repository_name }}" + source: "{{ repository_source_location }}" + state: present + register: adding_repository_again + +- name: test check adding repository defaults - idempotent + assert: + that: + - adding_repository_again is not changed + +- name: change InstallationPolicy - check mode + win_psrepository: + name: "{{ repository_name }}" + source: "{{ repository_source_location }}" + installation_policy: untrusted + check_mode: True + register: change_installation_policy_check + +- name: get result of change InstallationPolicy - check mode + ansible.windows.win_shell: '(Get-PSRepository -Name {{ repository_name | quote }}).InstallationPolicy' + changed_when: false + register: result_change_installation_policy_check + +- name: test change InstallationPolicy - check mode + assert: + that: + - change_installation_policy_check is changed + - result_change_installation_policy_check.stdout | trim == 'Trusted' + +- name: change InstallationPolicy + win_psrepository: + name: "{{ repository_name }}" + source: "{{ repository_source_location }}" + installation_policy: untrusted + register: change_installation_policy + +- name: get result of change InstallationPolicy + ansible.windows.win_shell: '(Get-PSRepository -Name {{ repository_name | quote }}).InstallationPolicy' + changed_when: false + register: result_change_installation_policy + +- name: test change InstallationPolicy + assert: + that: + - change_installation_policy is changed + - result_change_installation_policy.stdout | trim == 'Untrusted' + +- name: change InstallationPolicy - idempotent + win_psrepository: + name: "{{ repository_name }}" + source: "{{ repository_source_location }}" + installation_policy: untrusted + register: change_installation_policy_again + +- name: test change InstallationPolicy - idempotent + assert: + that: + - change_installation_policy_again is not changed + +- name: change source - check mode + win_psrepository: + name: "{{ repository_name }}" + source: "{{ repository_source_location2 }}" + state: present + check_mode: True + register: change_source_check + +- name: get result of change source - check mode + ansible.windows.win_shell: | + $repo = Get-PSRepository -Name {{ repository_name | quote }} + $repo.SourceLocation + $repo.InstallationPolicy + changed_when: False + register: result_change_source_check + +- name: test change source - check mode + assert: + that: + - change_source_check is changed + - result_change_source_check.stdout_lines[0] == repository_source_location + - result_change_source_check.stdout_lines[1] == 'Untrusted' + +- name: change source + win_psrepository: + name: "{{ repository_name }}" + source: "{{ repository_source_location2 }}" + state: present + register: change_source + +- name: get result of change source + ansible.windows.win_shell: | + $repo = Get-PSRepository -Name {{ repository_name | quote }} + $repo.SourceLocation + $repo.InstallationPolicy + changed_when: False + register: result_change_source + +- name: test change source + assert: + that: + - change_source is changed + - result_change_source.stdout_lines[0] == repository_source_location2 + - result_change_source.stdout_lines[1] == 'Untrusted' + +- name: remove repository - check mode + win_psrepository: + name: "{{ repository_name }}" + state: absent + check_mode: True + register: removing_repository_check + +- name: get result of remove repository - check mode + ansible.windows.win_shell: '(Get-PSRepository -Name {{ repository_name | quote }} -ErrorAction SilentlyContinue | Measure-Object).Count' + changed_when: false + register: result_removing_repository_check + +- name: test remove repository - check mode + assert: + that: + - removing_repository_check is changed + - result_removing_repository_check.stdout | trim == '1' + +- name: remove repository + win_psrepository: + name: "{{ repository_name }}" + state: absent + register: removing_repository + +- name: get result of remove repository + ansible.windows.win_shell: '(Get-PSRepository -Name {{ repository_name | quote }} -ErrorAction SilentlyContinue | Measure-Object).Count' + changed_when: false + register: result_removing_repository + +- name: test remove repository + assert: + that: + - removing_repository is changed + - result_removing_repository.stdout | trim == '0' + +- name: remove repository - idempotent + win_psrepository: + name: "{{ repository_name }}" + state: absent + register: remove_repository_again + +- name: test remove repository - idempotent + assert: + that: + - remove_repository_again is not changed diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository/tasks/update_and_force.yml b/ansible_collections/community/windows/tests/integration/targets/win_psrepository/tasks/update_and_force.yml new file mode 100644 index 000000000..ca78770c7 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository/tasks/update_and_force.yml @@ -0,0 +1,368 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: Tests for creating a repository with all locations set + block: + - name: Add a repository with all locations set (check mode) + win_psrepository: + name: "{{ repository_name }}" + source_location: "{{ repository_source_location }}" + publish_location: "{{ repository_publish_location }}" + script_source_location: "{{ repository_script_source_location }}" + script_publish_location: "{{ repository_script_publish_location }}" + register: all_register_check + check_mode: True + + - name: Assert that task is changed (check mode) + assert: + that: + - all_register_check is changed + + - import_tasks: get_repo_info.yml + + - name: Assert that the repository was not created (check mode) + assert: + that: + - not repo_result.exists + + - name: Add a repository with all locations set + win_psrepository: + name: "{{ repository_name }}" + source_location: "{{ repository_source_location }}" + publish_location: "{{ repository_publish_location }}" + script_source_location: "{{ repository_script_source_location }}" + script_publish_location: "{{ repository_script_publish_location }}" + register: all_register_check + + - name: Assert that task is changed + assert: + that: + - all_register_check is changed + + - import_tasks: get_repo_info.yml + + - name: Assert that the repository was created with expected locations + assert: + that: + - repo_result.exists + - repo_result.repo.SourceLocation == repository_source_location + - repo_result.repo.PublishLocation == repository_publish_location + - repo_result.repo.ScriptSourceLocation == repository_script_source_location + - repo_result.repo.ScriptPublishLocation == repository_script_publish_location + + - name: Add a repository with all locations set again (check mode) + win_psrepository: + name: "{{ repository_name }}" + source_location: "{{ repository_source_location }}" + publish_location: "{{ repository_publish_location }}" + script_source_location: "{{ repository_script_source_location }}" + script_publish_location: "{{ repository_script_publish_location }}" + register: all_register_check + check_mode: True + + - name: Assert that task is not changed + assert: + that: + - all_register_check is not changed + +- name: Tests for upating individual locations without changing existing ones + block: + - name: Update source location + block: + - name: Update source_location only (check mode) + win_psrepository: + name: "{{ repository_name }}" + source_location: "{{ repository_source_location_alt }}" + register: single_update_check + check_mode: True + + - name: Assert that task is changed (check mode) + assert: + that: + - single_update_check is changed + + - import_tasks: get_repo_info.yml + + - name: Assert that source location was not updated (check mode) + assert: + that: + - repo_result.exists + - repo_result.repo.SourceLocation != repository_source_location_alt + - repo_result.repo.PublishLocation == repository_publish_location + - repo_result.repo.ScriptSourceLocation == repository_script_source_location + - repo_result.repo.ScriptPublishLocation == repository_script_publish_location + + - name: Update source_location only + win_psrepository: + name: "{{ repository_name }}" + source_location: "{{ repository_source_location_alt }}" + register: single_update_check + + - name: Assert that task is changed + assert: + that: + - single_update_check is changed + + - import_tasks: get_repo_info.yml + + - name: Assert that source location was updated + assert: + that: + - repo_result.exists + - repo_result.repo.SourceLocation == repository_source_location_alt + - repo_result.repo.PublishLocation == repository_publish_location + - repo_result.repo.ScriptSourceLocation == repository_script_source_location + - repo_result.repo.ScriptPublishLocation == repository_script_publish_location + + - name: Update publish location + block: + - name: Update publish_location only (check mode) + win_psrepository: + name: "{{ repository_name }}" + publish_location: "{{ repository_publish_location_alt }}" + register: single_update_check + check_mode: True + + - name: Assert that task is changed (check mode) + assert: + that: + - single_update_check is changed + + - import_tasks: get_repo_info.yml + + - name: Assert that publish location was not updated (check mode) + assert: + that: + - repo_result.exists + - repo_result.repo.SourceLocation == repository_source_location_alt + - repo_result.repo.PublishLocation != repository_publish_location_alt + - repo_result.repo.ScriptSourceLocation == repository_script_source_location + - repo_result.repo.ScriptPublishLocation == repository_script_publish_location + + - name: Update publish_location only + win_psrepository: + name: "{{ repository_name }}" + publish_location: "{{ repository_publish_location_alt }}" + register: single_update_check + + - name: Assert that task is changed + assert: + that: + - single_update_check is changed + + - import_tasks: get_repo_info.yml + + - name: Assert that publish location was updated + assert: + that: + - repo_result.exists + - repo_result.repo.SourceLocation == repository_source_location_alt + - repo_result.repo.PublishLocation == repository_publish_location_alt + - repo_result.repo.ScriptSourceLocation == repository_script_source_location + - repo_result.repo.ScriptPublishLocation == repository_script_publish_location + + - name: Update script source location + block: + - name: Update script_source_location only (check mode) + win_psrepository: + name: "{{ repository_name }}" + script_source_location: "{{ repository_script_source_location_alt }}" + register: single_update_check + check_mode: True + + - name: Assert that task is changed (check mode) + assert: + that: + - single_update_check is changed + + - import_tasks: get_repo_info.yml + + - name: Assert that script source location was not updated (check mode) + assert: + that: + - repo_result.exists + - repo_result.repo.SourceLocation == repository_source_location_alt + - repo_result.repo.PublishLocation == repository_publish_location_alt + - repo_result.repo.ScriptSourceLocation != repository_script_source_location_alt + - repo_result.repo.ScriptPublishLocation == repository_script_publish_location + + - name: Update script_source_location only + win_psrepository: + name: "{{ repository_name }}" + script_source_location: "{{ repository_script_source_location_alt }}" + register: single_update_check + + - name: Assert that task is changed + assert: + that: + - single_update_check is changed + + - import_tasks: get_repo_info.yml + + - name: Assert that script source location was updated + assert: + that: + - repo_result.exists + - repo_result.repo.SourceLocation == repository_source_location_alt + - repo_result.repo.PublishLocation == repository_publish_location_alt + - repo_result.repo.ScriptSourceLocation == repository_script_source_location_alt + - repo_result.repo.ScriptPublishLocation == repository_script_publish_location + + - name: Update script publish location + block: + - name: Update script_publish_location only (check mode) + win_psrepository: + name: "{{ repository_name }}" + script_publish_location: "{{ repository_script_publish_location_alt }}" + register: single_update_check + check_mode: True + + - name: Assert that task is changed (check mode) + assert: + that: + - single_update_check is changed + + - import_tasks: get_repo_info.yml + + - name: Assert that script publish location was not updated (check mode) + assert: + that: + - repo_result.exists + - repo_result.repo.SourceLocation == repository_source_location_alt + - repo_result.repo.PublishLocation == repository_publish_location_alt + - repo_result.repo.ScriptSourceLocation == repository_script_source_location_alt + - repo_result.repo.ScriptPublishLocation != repository_script_publish_location_alt + + - name: Update script_publish_location only + win_psrepository: + name: "{{ repository_name }}" + script_publish_location: "{{ repository_script_publish_location_alt }}" + register: single_update_check + + - name: Assert that task is changed + assert: + that: + - single_update_check is changed + + - import_tasks: get_repo_info.yml + + - name: Assert that script publish location was updated + assert: + that: + - repo_result.exists + - repo_result.repo.SourceLocation == repository_source_location_alt + - repo_result.repo.PublishLocation == repository_publish_location_alt + - repo_result.repo.ScriptSourceLocation == repository_script_source_location_alt + - repo_result.repo.ScriptPublishLocation == repository_script_publish_location_alt + +- name: Tests for using force + block: + - name: Ensure repository is not unnecessarily re-registered (check mode) + win_psrepository: + force: True + name: "{{ repository_name }}" + source_location: "{{ repository_source_location_alt }}" + publish_location: "{{ repository_publish_location_alt }}" + script_source_location: "{{ repository_script_source_location_alt }}" + script_publish_location: "{{ repository_script_publish_location_alt }}" + register: force_check + check_mode: True + + - name: Assert that task is not changed (check mode) + assert: + that: + - force_check is not changed + + - import_tasks: get_repo_info.yml + + - name: Assert that repository settings remain identical (check mode) + assert: + that: + - repo_result.exists + - repo_result.repo.SourceLocation == repository_source_location_alt + - repo_result.repo.PublishLocation == repository_publish_location_alt + - repo_result.repo.ScriptSourceLocation == repository_script_source_location_alt + - repo_result.repo.ScriptPublishLocation == repository_script_publish_location_alt + + - name: Ensure repository is not unnecessarily re-registered + win_psrepository: + force: True + name: "{{ repository_name }}" + source_location: "{{ repository_source_location_alt }}" + publish_location: "{{ repository_publish_location_alt }}" + script_source_location: "{{ repository_script_source_location_alt }}" + script_publish_location: "{{ repository_script_publish_location_alt }}" + register: force_check + + - name: Assert that task is not changed + assert: + that: + - force_check is not changed + + - import_tasks: get_repo_info.yml + + - name: Assert that repository settings remain identical + assert: + that: + - repo_result.exists + - repo_result.repo.SourceLocation == repository_source_location_alt + - repo_result.repo.PublishLocation == repository_publish_location_alt + - repo_result.repo.ScriptSourceLocation == repository_script_source_location_alt + - repo_result.repo.ScriptPublishLocation == repository_script_publish_location_alt + + - name: Set only two locations (check mode) + win_psrepository: + force: True + name: "{{ repository_name }}" + source_location: "{{ repository_source_location }}" + script_source_location: "{{ repository_script_source_location }}" + register: force_check + check_mode: True + + - name: Assert that task is changed (check mode) + assert: + that: + - force_check is changed + + - import_tasks: get_repo_info.yml + + - name: Assert that repository settings remain identical (check mode) + assert: + that: + - repo_result.exists + - repo_result.repo.SourceLocation == repository_source_location_alt + - repo_result.repo.PublishLocation == repository_publish_location_alt + - repo_result.repo.ScriptSourceLocation == repository_script_source_location_alt + - repo_result.repo.ScriptPublishLocation == repository_script_publish_location_alt + + - name: Set only two locations + win_psrepository: + force: True + name: "{{ repository_name }}" + source_location: "{{ repository_source_location }}" + script_source_location: "{{ repository_script_source_location }}" + register: force_check + + - name: Assert that task is changed + assert: + that: + - force_check is changed + + - import_tasks: get_repo_info.yml + + - name: Assert that repository settings were updated + assert: + that: + - repo_result.exists + - repo_result.repo.SourceLocation == repository_source_location + - repo_result.repo.ScriptSourceLocation == repository_script_source_location + # these won't be blank because PowerShellGet tries to discover publish locations based on sources + - repo_result.repo.PublishLocation != repository_publish_location_alt + - repo_result.repo.ScriptPublishLocation != repository_script_publish_location_alt + +- name: remove repository (update and force tests) + win_psrepository: + name: "{{ repository_name }}" + state: absent diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/aliases b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/aliases new file mode 100644 index 000000000..215e0b069 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/aliases @@ -0,0 +1 @@ +shippable/windows/group4 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/defaults/main.yml new file mode 100644 index 000000000..3cfe90d68 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/defaults/main.yml @@ -0,0 +1,12 @@ +--- +user_password: 'gEYB#f74MslYcz&*1!*2WDM65!i&4*H' +test_users: + - name: repo_copy1 + - name: repo_copy2 + profile: '{{ remote_tmp_dir }}\odd_dir' + - name: copy_repo3 + +test_repos: + - PSGallery + - FakeGet + - FakeFileServer diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/files/SampleRepositories.xml b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/files/SampleRepositories.xml new file mode 100644 index 000000000..162c3ea8f --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/files/SampleRepositories.xml @@ -0,0 +1,94 @@ +<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04"> + <Obj RefId="0"> + <TN RefId="0"> + <T>System.Collections.Hashtable</T> + <T>System.Object</T> + </TN> + <DCT> + <En> + <S N="Key">FakeFileServer</S> + <Obj N="Value" RefId="1"> + <TN RefId="1"> + <T>Microsoft.PowerShell.Commands.PSRepository</T> + <T>System.Management.Automation.PSCustomObject</T> + <T>System.Object</T> + </TN> + <MS> + <S N="Name">FakeFileServer</S> + <S N="SourceLocation">\\domain\share\repo\</S> + <S N="PublishLocation">\\domain\share\repo\</S> + <S N="ScriptSourceLocation">\\domain\share\repo\</S> + <S N="ScriptPublishLocation">\\domain\share\repo\</S> + <B N="Trusted">false</B> + <B N="Registered">true</B> + <S N="InstallationPolicy">Untrusted</S> + <S N="PackageManagementProvider">NuGet</S> + <Obj N="ProviderOptions" RefId="2"> + <TNRef RefId="0" /> + <DCT /> + </Obj> + </MS> + </Obj> + </En> + <En> + <S N="Key">PSGallery</S> + <Obj N="Value" RefId="3"> + <TN RefId="2"> + <T>Deserialized.Microsoft.PowerShell.Commands.PSRepository</T> + <T>Deserialized.System.Management.Automation.PSCustomObject</T> + <T>Deserialized.System.Object</T> + </TN> + <MS> + <S N="Name">PSGallery</S> + <S N="SourceLocation">https://www.powershellgallery.com/api/v2</S> + <S N="PublishLocation">https://www.powershellgallery.com/api/v2/package/</S> + <S N="ScriptSourceLocation">https://www.powershellgallery.com/api/v2/items/psscript</S> + <S N="ScriptPublishLocation">https://www.powershellgallery.com/api/v2/package/</S> + <Obj N="Trusted" RefId="4"> + <TN RefId="3"> + <T>System.Management.Automation.SwitchParameter</T> + <T>System.ValueType</T> + <T>System.Object</T> + </TN> + <ToString>False</ToString> + <Props> + <B N="IsPresent">false</B> + </Props> + </Obj> + <B N="Registered">true</B> + <S N="InstallationPolicy">Untrusted</S> + <S N="PackageManagementProvider">NuGet</S> + <Obj N="ProviderOptions" RefId="5"> + <TN RefId="4"> + <T>Deserialized.System.Collections.Hashtable</T> + <T>Deserialized.System.Object</T> + </TN> + <DCT /> + </Obj> + </MS> + </Obj> + </En> + <En> + <S N="Key">FakeGet</S> + <Obj N="Value" RefId="6"> + <TNRef RefId="2" /> + <MS> + <S N="Name">FakeGet</S> + <S N="SourceLocation">https://www.example.org/api/nuget/v2</S> + <S N="PublishLocation">https://www.example.org/api/nuget/v2/package/</S> + <S N="ScriptSourceLocation">https://www.exampe.org/api/nuget/v2/items/psscript</S> + <S N="ScriptPublishLocation">https://www.example.org/api/nuget/v2/package/</S> + <B N="Trusted">true</B> + <B N="Registered">true</B> + <S N="InstallationPolicy">Trusted</S> + <S N="PackageManagementProvider">NuGet</S> + <Obj N="ProviderOptions" RefId="7"> + <TNRef RefId="4" /> + <DCT /> + </Obj> + </MS> + </Obj> + </En> + </DCT> + </Obj> +</Objs> diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/meta/main.yml new file mode 100644 index 000000000..acc7fbcae --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/meta/main.yml @@ -0,0 +1,3 @@ +dependencies: + - setup_remote_tmp_dir + - setup_win_psget diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/tasks/main.yml new file mode 100644 index 000000000..80d6039ea --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/tasks/main.yml @@ -0,0 +1,95 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: Reset + import_tasks: reset.yml + +- name: Main block + module_defaults: + community.windows.win_psrepository_copy: + source: '{{ remote_tmp_dir }}\SampleRepositories.xml' + block: + # avoiding the use of win_psrepository / Register-PSRepository due to its strict target checking. + # in the end, this module always looks at the XML file, so we'll just put a pre-baked one in place. + - name: Put our source file in place + ansible.windows.win_copy: + src: SampleRepositories.xml + dest: "{{ remote_tmp_dir }}" + force: yes + + - name: Copy repos with module defaults - check + community.windows.win_psrepository_copy: + register: status + check_mode: yes + + - assert: + that: status is changed + + - name: Copy repos with module defaults + community.windows.win_psrepository_copy: + register: status + + - assert: + that: status is changed + + - name: Copy repos with module defaults - again + community.windows.win_psrepository_copy: + register: status + + - assert: + that: status is not changed + + # these users should inherit the repositories via the Default profile + - name: Create test users + ansible.windows.win_user: + name: "{{ item.name }}" + profile: "{{ item.profile | default(omit) }}" + password: "{{ user_password }}" + groups: + - Administrators + loop: "{{ test_users }}" + + ##################################### + ## Begin inherited tests + + - name: Test inherited repos via Default profile + include_tasks: + file: test_by_user.yml + apply: + vars: + user: "{{ item }}" + expected_repos: "{{ test_repos }}" + loop: "{{ test_users | map(attribute='name') | list }}" + + ## End inherited tests + ##################################### + + - import_tasks: test_system_users.yml + + - import_tasks: test_exclude_profile.yml + + - import_tasks: test_include_profile.yml + + - import_tasks: test_exclude_repo.yml + + - import_tasks: test_include_repo.yml + + always: + - name: Reset + import_tasks: reset.yml + + - name: Remove test users + ansible.windows.win_user: + name: "{{ item.name }}" + state: absent + loop: "{{ test_users }}" + + - name: Remove test profiles + import_tasks: remove_test_profiles.yml + + - name: Remove sample file + ansible.windows.win_file: + path: '{{ remote_tmp_dir }}\SampleRepositories.xml' + state: absent diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/tasks/remove_test_profiles.yml b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/tasks/remove_test_profiles.yml new file mode 100644 index 000000000..ab15d40fb --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/tasks/remove_test_profiles.yml @@ -0,0 +1,42 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +# removing a user doesn't remove their profile (which includes the filesystem directory and registry key). +# without cleaning up, every CI run will create a new path and key and they will build up over time. +# this finds all profiles on the system, via the registry, whose path basename starts with one of our test users +# and then deletes the directory; if that was succesful, it deletes the registry key too. +# +# As a result this should also clean up after any old runs that left behind profiles, so an interrupted run +# that didn't run the rescue block will still get cleaned up by the next when this runs again. + +- name: Remove all the test user profiles from the system + become: yes + become_user: SYSTEM + become_method: runas + ansible.windows.win_shell: | + $names = @' + {{ test_users | map(attribute='name') | list | to_json }} + '@ | ConvertFrom-Json + $regPL = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' + $profiles = Get-ChildItem -LiteralPath $regPL + + $profiles | ForEach-Object -Process { + $path = ($_ | Get-ItemProperty | Select-Object -ExpandProperty ProfileImagePath) -as [System.IO.DirectoryInfo] + :search foreach ($target in $names) { + if ($path.Name -match "^$target") { + $delReg=$true + if ($path.Exists) { + & cmd.exe /c rd /s /q $path.FullName + if (-not ($delReg=$?)) { + Write-Warning -Message "Couldn't remove '$path'" + } + } + if ($delReg) { + $_ | Remove-Item -Force -ErrorAction SilentlyContinue + } + break search + } + } + } diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/tasks/reset.yml b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/tasks/reset.yml new file mode 100644 index 000000000..d985c2950 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/tasks/reset.yml @@ -0,0 +1,22 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: Remove the psrepositories for all profiles on the system + become: yes + become_user: SYSTEM + become_method: runas + ansible.windows.win_shell: | + $regPL = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' + $default = Get-ItemProperty -LiteralPath $regPL | Select-Object -ExpandProperty Default + $profiles = ( + @($default) + + (Get-ChildItem -LiteralPath $regPL | Get-ItemProperty | Select-Object -ExpandProperty ProfileImagePath) + ) -as [System.IO.DirectoryInfo[]] + $profiles | + Where-Object -Property Exists -EQ $true | + ForEach-Object -Process { + $p = [System.IO.Path]::Combine($_.FullName, 'AppData\Local\Microsoft\Windows\PowerShell\PowerShellGet\PSRepositories.xml') + Remove-Item -LiteralPath $p -Force -ErrorAction SilentlyContinue + } diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/tasks/test_by_user.yml b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/tasks/test_by_user.yml new file mode 100644 index 000000000..f5f76d20b --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/tasks/test_by_user.yml @@ -0,0 +1,32 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: Get repository info + become: yes + become_user: "{{ user }}" + become_method: runas + community.windows.win_psrepository_info: + register: repos + +- name: Ensure expected repositories are present + assert: + that: repo in (repos.repositories | map(attribute='name') | list) + success_msg: "expected repository '{{ repo }}' was found for user '{{ user }}'" + fail_msg: "expected repository '{{ repo }}' was not found for user '{{ user }}'" + loop: "{{ expected_repos }}" + loop_control: + loop_var: repo + +- name: Ensure present repositories are expected + assert: + that: repo in expected_repos or repo == 'PSGallery' + # although not completely consistent depending on OS and/or PowerShellGet versions + # often calling Get-PSRepository will register PSGallery if it's missing, so we + # must be prepared for it to show up here "unexpectedly" by way of us querying :( + success_msg: "found expected repository '{{ repo }}' for user '{{ user }}'" + fail_msg: "found unexpected repository '{{ repo }}' for user '{{ user }}'" + loop: "{{ repos.repositories | map(attribute='name') | list }}" + loop_control: + loop_var: repo diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/tasks/test_exclude_profile.yml b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/tasks/test_exclude_profile.yml new file mode 100644 index 000000000..19da14e1d --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/tasks/test_exclude_profile.yml @@ -0,0 +1,58 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +##################################### +## Begin filter profile (exclusive) tests +- name: Reset + import_tasks: reset.yml + +- name: Copy repos with excluded profiles - check + community.windows.win_psrepository_copy: + exclude_profiles: 'repo*' + register: status + check_mode: yes + +- assert: + that: status is changed + +- name: Copy repos with excluded profiles + community.windows.win_psrepository_copy: + exclude_profiles: 'repo*' + register: status + +- assert: + that: status is changed + +- name: Copy repos with excluded profiles - again + community.windows.win_psrepository_copy: + exclude_profiles: 'repo*' + register: status + +- assert: + that: status is not changed + +- name: Test filtered profiles (excluded) + include_tasks: + file: test_by_user.yml + apply: + vars: + user: "{{ item }}" + expected_repos: [] + loop: + - repo_copy1 + - repo_copy2 + +- name: Test filtered profiles (not excluded) + include_tasks: + file: test_by_user.yml + apply: + vars: + user: "{{ item }}" + expected_repos: "{{ test_repos }}" + loop: + - copy_repo3 + +## End filter profile (exclusive) tests +##################################### diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/tasks/test_exclude_repo.yml b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/tasks/test_exclude_repo.yml new file mode 100644 index 000000000..c85190243 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/tasks/test_exclude_repo.yml @@ -0,0 +1,49 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +##################################### +## Begin filter repo (exclusive) tests + +- name: Reset + import_tasks: reset.yml + +- name: Copy filtered repos by exclusion - check + community.windows.win_psrepository_copy: + exclude: '*Server' + register: status + check_mode: yes + +- assert: + that: status is changed + +- name: Copy filtered repos by exclusion + community.windows.win_psrepository_copy: + exclude: '*Server' + register: status + +- assert: + that: status is changed + +- name: Copy filtered repos by exclusion - again + community.windows.win_psrepository_copy: + exclude: '*Server' + register: status + +- assert: + that: status is not changed + +- name: Test filtered repos (included) + include_tasks: + file: test_by_user.yml + apply: + vars: + user: "{{ item }}" + expected_repos: + - PSGallery + - FakeGet + loop: "{{ test_users | map(attribute='name') | list }}" + +## End filter repo (exclusive) tests +##################################### diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/tasks/test_include_profile.yml b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/tasks/test_include_profile.yml new file mode 100644 index 000000000..6e3d47b97 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/tasks/test_include_profile.yml @@ -0,0 +1,59 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +##################################### +## Begin filter profile (inclusive) tests + +- name: Reset + import_tasks: reset.yml + +- name: Copy repos with filtered profiles - check + community.windows.win_psrepository_copy: + profiles: 'repo*' + register: status + check_mode: yes + +- assert: + that: status is changed + +- name: Copy repos with filtered profiles + community.windows.win_psrepository_copy: + profiles: 'repo*' + register: status + +- assert: + that: status is changed + +- name: Copy repos with filtered profiles - again + community.windows.win_psrepository_copy: + profiles: 'repo*' + register: status + +- assert: + that: status is not changed + +- name: Test filtered profiles (included) + include_tasks: + file: test_by_user.yml + apply: + vars: + user: "{{ item }}" + expected_repos: "{{ test_repos }}" + loop: + - repo_copy1 + - repo_copy2 + +- name: Test filtered profiles (not included) + include_tasks: + file: test_by_user.yml + apply: + vars: + user: "{{ item }}" + expected_repos: [] + loop: + - copy_repo3 + +## End filter profile (inclusive) tests +##################################### diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/tasks/test_include_repo.yml b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/tasks/test_include_repo.yml new file mode 100644 index 000000000..3bc392931 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/tasks/test_include_repo.yml @@ -0,0 +1,49 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +##################################### +## Begin filter repo (inclusive) tests + +- name: Reset + import_tasks: reset.yml + +- name: Copy filtered repos - check + community.windows.win_psrepository_copy: + name: '*G*' + register: status + check_mode: yes + +- assert: + that: status is changed + +- name: Copy filtered repos + community.windows.win_psrepository_copy: + name: '*G*' + register: status + +- assert: + that: status is changed + +- name: Copy filtered repos - again + community.windows.win_psrepository_copy: + name: '*G*' + register: status + +- assert: + that: status is not changed + +- name: Test filtered repos (included) + include_tasks: + file: test_by_user.yml + apply: + vars: + user: "{{ item }}" + expected_repos: + - PSGallery + - FakeGet + loop: "{{ test_users | map(attribute='name') | list }}" + +## End filter repo (inclusive) tests +##################################### diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/tasks/test_system_users.yml b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/tasks/test_system_users.yml new file mode 100644 index 000000000..446ad4d1c --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_copy/tasks/test_system_users.yml @@ -0,0 +1,68 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +##################################### +## Begin system user tests + +- name: Reset + import_tasks: reset.yml + +- name: Copy repos only to special system users - check + community.windows.win_psrepository_copy: + profiles: + - '*Service' + - 'systemprofile' + exclude_profiles: [] + register: status + check_mode: yes + +- assert: + that: status is changed + +- name: Copy repos only to special system users + community.windows.win_psrepository_copy: + profiles: + - '*Service' + - 'systemprofile' + exclude_profiles: [] + register: status + +- assert: + that: status is changed + +- name: Copy repos only to special system users - again + community.windows.win_psrepository_copy: + profiles: + - '*Service' + - 'systemprofile' + exclude_profiles: [] + register: status + +- assert: + that: status is not changed + +- name: Test system users + include_tasks: + file: test_by_user.yml + apply: + vars: + user: "{{ 'SYSTEM' if item == 'systemprofile' else item }}" + expected_repos: "{{ test_repos }}" + loop: + - systemprofile + - LocalService + - NetworkService + +- name: Test other users + include_tasks: + file: test_by_user.yml + apply: + vars: + user: "{{ item }}" + expected_repos: [] + loop: "{{ test_users | map(attribute='name') | list }}" + +## End system user tests +##################################### diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository_info/aliases b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_info/aliases new file mode 100644 index 000000000..748bc3ed5 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_info/aliases @@ -0,0 +1,3 @@ +shippable/windows/group3 +needs/httptester +unstable diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository_info/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_info/defaults/main.yml new file mode 100644 index 000000000..13dac5dee --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_info/defaults/main.yml @@ -0,0 +1,10 @@ +--- +run_check_mode: False +suffix: "{{ '(check mode)' if run_check_mode else '' }}" +default_repository_name: PSGallery + +second_repository_name: PowerShellGetDemo +second_repository_source_location: https://www.nuget.org/api/v2 + +third_repository_name: OtherRepo +third_repository_source_location: http://{{ httpbin_host }}/get diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository_info/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_info/meta/main.yml new file mode 100644 index 000000000..f050654c7 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_info/meta/main.yml @@ -0,0 +1,3 @@ +dependencies: + - setup_http_tests + - setup_win_psget diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository_info/tasks/contains_all_fields.yml b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_info/tasks/contains_all_fields.yml new file mode 100644 index 000000000..6f3e6f500 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_info/tasks/contains_all_fields.yml @@ -0,0 +1,21 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: Check for key ('{{ key }}') in result + assert: + that: key in dict_to_check + loop_control: + loop_var: key + loop: + - name + - installation_policy + - package_management_provider + - provider_options + - publish_location + - source_location + - script_source_location + - script_publish_location + - registered + - trusted diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository_info/tasks/empty.yml b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_info/tasks/empty.yml new file mode 100644 index 000000000..9bc6d4c64 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_info/tasks/empty.yml @@ -0,0 +1,19 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: Tests against the empty set of repositories + block: + - name: Get repository info {{ suffix }} + win_psrepository_info: + register: repo_info + + - name: Assert that the correct structure is returned {{ suffix }} + assert: + that: + - repo_info.repositories is defined + - repo_info.repositories is sequence() + - repo_info.repositories | length == 0 + # block + check_mode: "{{ run_check_mode }}" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository_info/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_info/tasks/main.yml new file mode 100644 index 000000000..72427e211 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_info/tasks/main.yml @@ -0,0 +1,51 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: Unregister all repositories + ansible.windows.win_shell: | + Get-PSRepository | Unregister-PSRepository + +- block: + - name: Run Empty Tests + import_tasks: empty.yml + + - name: Run Empty Tests (check mode) + import_tasks: empty.yml + vars: + run_check_mode: True + + - name: Add the default repository + ansible.windows.win_shell: | + Register-PSRepository -Default + + - name: Single Repository Tests + import_tasks: single.yml + + - name: Single Repository Tests (check mode) + import_tasks: single.yml + vars: + run_check_mode: True + + - name: Add two more repositories + ansible.windows.win_shell: | + Register-PSRepository -Name '{{ second_repository_name }}' -SourceLocation '{{ second_repository_source_location }}' + Register-PSRepository -Name '{{ third_repository_name }}' -SourceLocation '{{ third_repository_source_location }}' + + - name: Multi Repository Tests + import_tasks: multiple.yml + + - name: Multi Repository Tests (check mode) + import_tasks: multiple.yml + vars: + run_check_mode: True + + always: + - name: Unregister all repositories + ansible.windows.win_shell: | + Get-PSRepository | Unregister-PSRepository + + - name: Ensure only the default repository remains + ansible.windows.win_shell: | + Register-PSRepository -Default diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository_info/tasks/multiple.yml b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_info/tasks/multiple.yml new file mode 100644 index 000000000..8c5b986e7 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_info/tasks/multiple.yml @@ -0,0 +1,37 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: Tests against mutiple repositories + block: + - name: Get all repository info {{ suffix }} + win_psrepository_info: + register: repo_info + + - name: Assert that the correct structure is returned {{ suffix }} + assert: + that: + - repo_info.repositories is defined + - repo_info.repositories is sequence() + - repo_info.repositories | length == 3 + + - include_tasks: contains_all_fields.yml + vars: + dict_to_check: "{{ item }}" + loop: "{{ repo_info.repositories }}" + + - name: Get two repositories with a filter {{ suffix }} + win_psrepository_info: + name: P* + register: repo_info + + - name: Assert that the correct two repositories were returned {{ suffix }} + assert: + that: + - repo_info.repositories | length == 2 + - default_repository_name in (repo_info.repositories | map(attribute='name') | list) + - second_repository_name in (repo_info.repositories | map(attribute='name') | list) + + # block + check_mode: "{{ run_check_mode }}" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psrepository_info/tasks/single.yml b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_info/tasks/single.yml new file mode 100644 index 000000000..00071ec55 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psrepository_info/tasks/single.yml @@ -0,0 +1,26 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: Tests against a single repository ({{ default_repository_name }}) + block: + - name: Get repository info {{ suffix }} + win_psrepository_info: + name: "{{ default_repository_name }}" + register: repo_info + + - name: Assert that the correct structure is returned {{ suffix }} + assert: + that: + - repo_info.repositories is defined + - repo_info.repositories is sequence() + - repo_info.repositories | length == 1 + - repo_info.repositories[0].name == default_repository_name + + - include_tasks: contains_all_fields.yml + vars: + dict_to_check: "{{ repo_info.repositories[0] }}" + + # block + check_mode: "{{ run_check_mode }}" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psscript/aliases b/ansible_collections/community/windows/tests/integration/targets/win_psscript/aliases new file mode 100644 index 000000000..4f4664b68 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psscript/aliases @@ -0,0 +1 @@ +shippable/windows/group5 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psscript/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_psscript/defaults/main.yml new file mode 100644 index 000000000..010348236 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psscript/defaults/main.yml @@ -0,0 +1,26 @@ +--- +repos: + - name: Repo1 + path: '{{ remote_tmp_dir }}\Repo1' + policy: trusted + scripts: + - Test-One + - Test-Multi + + - name: Repo2 + path: '{{ remote_tmp_dir }}\Repo2' + policy: untrusted + scripts: + - Test-Two + - Test-Multi + +versions: + - 1.0.0 + - 1.0.9 + - 1.0.10 + - 10.0.0 + - 10.0.9 + - 10.0.10 + +earliest_version: 1.0.0 +latest_version: 10.0.10 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psscript/handlers/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_psscript/handlers/main.yml new file mode 100644 index 000000000..83c4a490c --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psscript/handlers/main.yml @@ -0,0 +1,22 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: Clear Repositories + ansible.windows.win_shell: | + Get-PSRepository | Unregister-PSRepository + +- name: Add PSGallery + ansible.windows.win_shell: | + Register-PSRepository -Default + +- name: Clear Installed Scripts + ansible.windows.win_shell: | + Get-InstalledScript | Uninstall-Script -Force + +- name: Remove Directories + ansible.windows.win_file: + path: '{{ item.path }}' + state: absent + loop: "{{ repos }}" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psscript/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_psscript/meta/main.yml new file mode 100644 index 000000000..acc7fbcae --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psscript/meta/main.yml @@ -0,0 +1,3 @@ +dependencies: + - setup_remote_tmp_dir + - setup_win_psget diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psscript/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_psscript/tasks/main.yml new file mode 100644 index 000000000..97779ae0f --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psscript/tasks/main.yml @@ -0,0 +1,13 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: Setup local repositories + import_tasks: setup_repos.yml + tags: + - always + - setup + +- name: Run Tests + import_tasks: tests.yml
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psscript/tasks/script_info.yml b/ansible_collections/community/windows/tests/integration/targets/win_psscript/tasks/script_info.yml new file mode 100644 index 000000000..d16993460 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psscript/tasks/script_info.yml @@ -0,0 +1,81 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: Get installed scripts + ansible.windows.win_shell: | + $pMachine = "$env:ProgramFiles\WindowsPowerShell\Scripts" + # 2012/2012R2 test environments seem to have blank values for some environment vars + $userStub = 'Documents\WindowsPowerShell\Scripts' + $userBase = if ($Home) { + $Home + } + elseif ($env:UserProfile) { + $env:UserProfile + } + elseif ($env:HomeDrive -and $env:HomePath) { + "${env:HomeDrive}${env:HomePath}\$userStub" + } + elseif ($env:UserName) { + $root = if ($env:SystemDrive) { + $env:SystemDrive + } + elseif ($emv:HomeDrive) { + $env:HomeDrive + } + else { + 'C:' + } + "${root}\Users\${env:UserName}" + } + $pUser = "${userBase}\${userStub}" + + $scripts = Get-InstalledScript | + Select-Object Name,Version,InstalledLocation,Repository,@{ + Name = 'current_user' + Expression = { $_.InstalledLocation -eq $pUser } + }, + @{ + Name = 'all_users' + Expression = { $_.InstalledLocation -eq $pMachine } + }, + @{ + # this is for troubleshooting tests + # 2012/2012R2 test environments seem to have blank values for some environment vars + Name = 'paths_to_compare' + Expression = { + @{ + pUser = $pUser + pMachine = $pMachine + home = $Home + homedrive = $env:HOMEDRIVE + homepath = $env:HOMEPATH + providerhome = (Get-PSProvider -PSProvider FileSystem).Home + userprofile = $env:UserProfile + username = $env:UserName + } + } + } + + if (-not $scripts) { + $scripts = @() + } + + ConvertTo-Json -Depth 5 -InputObject ([object[]]$scripts) + register: _raw_info + +- name: Set script info var + set_fact: + "{{ script_info_var | default('scripts') }}": "{{ + dict( + _raw_info.stdout + | from_json + | map(attribute='Name') + | list + | zip( + _raw_info.stdout + | from_json + ) + ) + }}" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psscript/tasks/setup_repos.yml b/ansible_collections/community/windows/tests/integration/targets/win_psscript/tasks/setup_repos.yml new file mode 100644 index 000000000..94dea1871 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psscript/tasks/setup_repos.yml @@ -0,0 +1,58 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: Unregister all repositories + ansible.windows.win_shell: | + Get-PSRepository | Unregister-PSRepository + notify: Add PSGallery + +- name: Create repo folders + ansible.windows.win_file: + path: '{{ item.path }}' + state: directory + loop: "{{ repos }}" + notify: Remove Directories + +- name: Register repos + win_psrepository: + name: '{{ item.name }}' + source: '{{ item.path }}' + installation_policy: '{{ item.policy }}' + notify: Clear Repositories + loop: "{{ repos }}" + +- name: Create and Publish Scripts + ansible.windows.win_shell: | + $tmp = '{{ remote_tmp_dir }}' + + # it looks like Tls12 is not added during the nuget bootstrapping process + # hitting connection closed errors during some tests. May be related to this: + # https://devblogs.microsoft.com/nuget/deprecating-tls-1-0-and-1-1-on-nuget-org/ + [System.Net.ServicePointManager]::SecurityProtocol = ` + [System.Net.ServicePointManager]::SecurityProtocol -bor + [System.Net.SecurityProtocolType]::SystemDefault -bor + [System.Net.SecurityProtocolType]::Tls11 -bor + [System.Net.SecurityProtocolType]::Tls12 + + {% for version in versions %} + {% for repo in repos %} + {% for script in repo.scripts %} + + $script = '{{ script }}' + $repo = '{{ repo.name }}' + $ver = '{{ version }}' + $file = "${script}.ps1" + $out = $tmp | Join-Path -ChildPath $file + + try { + New-ScriptFileInfo -Description $script -Version $ver -Path $out -Force + Publish-Script -Path $out -Repository $repo -Force + } finally { + Remove-Item -LiteralPath $out -Force -ErrorAction SilentlyContinue + } + + {% endfor %} + {% endfor %} + {% endfor %} diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psscript/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_psscript/tasks/tests.yml new file mode 100644 index 000000000..7376b3fa5 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psscript/tasks/tests.yml @@ -0,0 +1,555 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: Basic Tests Script 1 + tags: + - basic1 + vars: + script: Test-One + scope: all_users + expected: + repo: Repo1 + version: "{{ latest_version }}" + block: + - name: Install a script (check mode) + win_psscript: + name: "{{ script }}" + scope: "{{ scope }}" + register: result + check_mode: True + + - name: Assert task is changed (check mode) + assert: + that: result is changed + + - import_tasks: script_info.yml + + - name: Ensure nothing was installed (check mode) + assert: + that: + - scripts | length == 0 + - script not in scripts + + - name: Install a script + win_psscript: + name: "{{ script }}" + scope: "{{ scope }}" + register: result + notify: Clear Installed Scripts + + - name: Assert task is changed + assert: + that: result is changed + + - import_tasks: script_info.yml + + - name: Ensure script was installed + assert: + that: + - scripts | length == 1 + - script in scripts + - scripts[script].Version is version(expected.version, '==') + - scripts[script].Repository == expected.repo + - scripts[script][scope] + + - name: Install script again (check mode) + win_psscript: + name: "{{ script }}" + scope: "{{ scope }}" + register: result + check_mode: True + + - name: Assert task is not changed (check mode) + assert: + that: result is not changed + +- name: Basic Tests Script 2 + tags: + - basic2 + vars: + script: Test-Two + scope: current_user + expected: + repo: Repo2 + version: "{{ latest_version }}" + block: + - name: Install a script (check mode) + win_psscript: + name: "{{ script }}" + scope: "{{ scope }}" + register: result + check_mode: True + + - name: Assert task is changed (check mode) + assert: + that: result is changed + + - import_tasks: script_info.yml + + - name: Ensure nothing was installed (check mode) + assert: + that: + - scripts | length == 1 + - script not in scripts + + - name: Install a script + win_psscript: + name: "{{ script }}" + scope: "{{ scope }}" + register: result + notify: Clear Installed Scripts + + - name: Assert task is changed + assert: + that: result is changed + + - import_tasks: script_info.yml + + - name: Ensure script was installed + assert: + that: + - scripts | length == 2 + - script in scripts + - scripts[script].Version is version(expected.version, '==') + - scripts[script].Repository == expected.repo + - scripts[script][scope] + + - name: Install script again (check mode) + win_psscript: + name: "{{ script }}" + scope: "{{ scope }}" + register: result + check_mode: True + + - name: Assert task is not changed (check mode) + assert: + that: result is not changed + +- name: Removal Tests Script 1 + tags: + - remove1 + vars: + script: Test-One + state: absent + block: + - name: Remove Script (check mode) + win_psscript: + name: "{{ script }}" + state: "{{ state }}" + register: result + check_mode: True + + - name: Assert task is changed (check mode) + assert: + that: result is changed + + - import_tasks: script_info.yml + + - name: Ensure script was not removed (check mode) + assert: + that: + - scripts | length == 2 + - script in scripts + + - name: Remove Script + win_psscript: + name: "{{ script }}" + state: "{{ state }}" + register: result + + - name: Assert task is changed + assert: + that: result is changed + + - import_tasks: script_info.yml + + - name: Ensure script was removed + assert: + that: + - scripts | length == 1 + - script not in scripts + + - name: Remove Script Again (check mode) + win_psscript: + name: "{{ script }}" + state: "{{ state }}" + register: result + check_mode: True + + - name: Assert task is not changed (check mode) + assert: + that: result is not changed + +- name: Removal Tests Script 2 + tags: + - remove2 + vars: + script: Test-Two + state: absent + block: + - name: Remove Script (check mode) + win_psscript: + name: "{{ script }}" + state: "{{ state }}" + register: result + check_mode: True + + - name: Assert task is changed (check mode) + assert: + that: result is changed + + - import_tasks: script_info.yml + + - name: Ensure script was not removed (check mode) + assert: + that: + - scripts | length == 1 + - script in scripts + + - name: Remove Script + win_psscript: + name: "{{ script }}" + state: "{{ state }}" + register: result + + - name: Assert task is changed + assert: + that: result is changed + + - import_tasks: script_info.yml + + - name: Ensure script was removed + assert: + that: + - scripts | length == 0 + - script not in scripts + + - name: Remove Script Again (check mode) + win_psscript: + name: "{{ script }}" + state: "{{ state }}" + register: result + check_mode: True + + - name: Assert task is not changed (check mode) + assert: + that: result is not changed + +- name: Tests for Script in Multiple Repos + tags: + - multi + vars: + script: Test-Multi + scope: all_users + expected: + repo: Repo2 + version: "{{ latest_version }}" + block: + - name: Install a script from multiple repos + win_psscript: + name: "{{ script }}" + scope: "{{ scope }}" + register: result + failed_when: result.msg is not search('Multiple scripts found') + + - name: Install a script from multiple repos by chosing one (check mode) + win_psscript: + name: "{{ script }}" + scope: "{{ scope }}" + repository: "{{ expected.repo }}" + register: result + check_mode: True + + - name: Assert task is changed (check mode) + assert: + that: result is changed + + - import_tasks: script_info.yml + + - name: Ensure nothing was installed (check mode) + assert: + that: + - scripts | length == 0 + - script not in scripts + + - name: Install a script from multiple repos by chosing one + win_psscript: + name: "{{ script }}" + scope: "{{ scope }}" + repository: "{{ expected.repo }}" + register: result + notify: Clear Installed Scripts + + - name: Assert task is changed + assert: + that: result is changed + + - import_tasks: script_info.yml + + - name: Ensure script was installed + assert: + that: + - scripts | length == 1 + - script in scripts + - scripts[script].Version is version(expected.version, '==') + - scripts[script].Repository == expected.repo + - scripts[script][scope] + + - name: Install script again (check mode) + win_psscript: + name: "{{ script }}" + scope: "{{ scope }}" + repository: "{{ expected.repo }}" + register: result + check_mode: True + + - name: Assert task is not changed (check mode) + assert: + that: result is not changed + +- name: Exact Version Tests + tags: + - exact + vars: + script: Test-Two + scope: current_user + expected: + repo: Repo2 + version: "{{ earliest_version }}" + block: + - name: Install a script (check mode) + win_psscript: + name: "{{ script }}" + scope: "{{ scope }}" + required_version: "{{ expected.version }}" + register: result + check_mode: True + + - name: Assert task is changed (check mode) + assert: + that: result is changed + + - import_tasks: script_info.yml + + - name: Ensure nothing was installed (check mode) + assert: + that: + - script not in scripts + + - name: Install a script + win_psscript: + name: "{{ script }}" + scope: "{{ scope }}" + required_version: "{{ expected.version }}" + register: result + notify: Clear Installed Scripts + + - name: Assert task is changed + assert: + that: result is changed + + - import_tasks: script_info.yml + + - name: Ensure script was installed + assert: + that: + - script in scripts + - scripts[script].Version is version(expected.version, '==') + - scripts[script].Repository == expected.repo + - scripts[script][scope] + + - name: Install script again (check mode) + win_psscript: + name: "{{ script }}" + scope: "{{ scope }}" + required_version: "{{ expected.version }}" + register: result + check_mode: True + + - name: Assert task is not changed (check mode) + assert: + that: result is not changed + +- name: Minimum / Maximum Version Tests + tags: + - minmax + - present + vars: + script: Test-Two + scope: current_user + minver: 1.0.1 + maxver: 2.0.0 + expected: + repo: Repo2 + version: "1.0.10" + block: + - name: Install a script (check mode) + win_psscript: + name: "{{ script }}" + scope: "{{ scope }}" + minimum_version: "{{ minver }}" + maximum_version: "{{ maxver }}" + register: result + check_mode: True + + - name: Assert task is changed (check mode) + assert: + that: result is changed + + - import_tasks: script_info.yml + + - name: Ensure nothing was installed (check mode) + assert: + that: + - script not in scripts or scripts[script].Version is version(expected.version, '<') + + - name: Install a script + win_psscript: + name: "{{ script }}" + scope: "{{ scope }}" + minimum_version: "{{ minver }}" + maximum_version: "{{ maxver }}" + register: result + notify: Clear Installed Scripts + + - name: Assert task is changed + assert: + that: result is changed + + - import_tasks: script_info.yml + + - name: Ensure script was installed + assert: + that: + - script in scripts + - scripts[script].Version is version(expected.version, '==') + - scripts[script].Repository == expected.repo + - scripts[script][scope] + + - name: Install script again (check mode) + win_psscript: + name: "{{ script }}" + scope: "{{ scope }}" + minimum_version: "{{ minver }}" + maximum_version: "{{ maxver }}" + register: result + check_mode: True + + - name: Assert task is not changed (check mode) + assert: + that: result is not changed + +- name: State Present Tests + tags: + - present + vars: + script: Test-Two + state: present + scope: all_users # implied + curver: 1.0.10 + expected: + repo: Repo2 + version: "{{ curver }}" + block: + - name: Install present script (check mode) + win_psscript: + name: "{{ script }}" + state: "{{ state }}" + register: result + check_mode: True + + - name: Assert task is not changed (check mode) + assert: + that: result is not changed + + - import_tasks: script_info.yml + + - name: Ensure nothing was installed (check mode) + assert: + that: + - script in scripts + - scripts[script].Version is version(expected.version, '==') + - scripts[script].Repository == expected.repo + + - name: Install present script + win_psscript: + name: "{{ script }}" + state: "{{ state }}" + register: result + notify: Clear Installed Scripts + + - name: Assert task is not changed + assert: + that: result is not changed + + - import_tasks: script_info.yml + + - name: Ensure script was not installed + assert: + that: + - script in scripts + - scripts[script].Version is version(expected.version, '==') + - scripts[script].Repository == expected.repo + +- name: State Latest Tests + tags: + - latest + vars: + script: Test-Two + state: latest + scope: all_users # implied + expected: + repo: Repo2 + version: "10.0.10" + block: + - name: Install latest script (check mode) + win_psscript: + name: "{{ script }}" + state: "{{ state }}" + register: result + check_mode: True + + - name: Assert task is changed (check mode) + assert: + that: result is changed + + - import_tasks: script_info.yml + + - name: Ensure nothing was installed (check mode) + assert: + that: + - script not in scripts or scripts[script].Version is version(expected.version, '<') + + - name: Install latest script + win_psscript: + name: "{{ script }}" + state: "{{ state }}" + register: result + notify: Clear Installed Scripts + + - name: Assert task is changed + assert: + that: result is changed + + - import_tasks: script_info.yml + + - name: Ensure script was installed + assert: + that: + - script in scripts + - scripts[script].Version is version(expected.version, '==') + - scripts[script].Repository == expected.repo + - scripts[script][scope] + + - name: Install latest script again (check mode) + win_psscript: + name: "{{ script }}" + state: "{{ state }}" + register: result + check_mode: True + + - name: Assert task is not changed (check mode) + assert: + that: result is not changed diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/aliases b/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/aliases new file mode 100644 index 000000000..3cf5b97e8 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/aliases @@ -0,0 +1 @@ +shippable/windows/group3 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/defaults/main.yml new file mode 100644 index 000000000..786fb5477 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/defaults/main.yml @@ -0,0 +1,34 @@ +--- +run_check_mode: False +suffix: "{{ '(check mode)' if run_check_mode else '' }}" +repository_name: Repo +repo_path: "{{ remote_tmp_dir }}\\repo\\" + +sample_scripts: + - Test-RPC + - Upgrade-PowerShell + - Install-WMF3Hotfix + - Install-Git + +expected_fields: + - name + - version + - installed_location + - author + - copyright + - company_name + - description + - dependencies + - icon_uri + - license_uri + - project_uri + - repository_source_location + - repository + - release_notes + - installed_date + - published_date + - updated_date + - package_management_provider + - tags + - power_shell_get_format_version + - additional_metadata diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/files/install-git.1.0.5.nupkg b/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/files/install-git.1.0.5.nupkg Binary files differnew file mode 100644 index 000000000..0dc10a004 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/files/install-git.1.0.5.nupkg diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/files/install-wmf3hotfix.1.0.0.nupkg b/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/files/install-wmf3hotfix.1.0.0.nupkg Binary files differnew file mode 100644 index 000000000..2728fb4d5 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/files/install-wmf3hotfix.1.0.0.nupkg diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/files/test-rpc.1.0.0.nupkg b/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/files/test-rpc.1.0.0.nupkg Binary files differnew file mode 100644 index 000000000..caf11314f --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/files/test-rpc.1.0.0.nupkg diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/files/upgrade-powershell.1.0.0.nupkg b/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/files/upgrade-powershell.1.0.0.nupkg Binary files differnew file mode 100644 index 000000000..d260004a2 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/files/upgrade-powershell.1.0.0.nupkg diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/meta/main.yml new file mode 100644 index 000000000..acc7fbcae --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/meta/main.yml @@ -0,0 +1,3 @@ +dependencies: + - setup_remote_tmp_dir + - setup_win_psget diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/tasks/common.yml b/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/tasks/common.yml new file mode 100644 index 000000000..b4cdda063 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/tasks/common.yml @@ -0,0 +1,37 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: Assert that the correct structure is returned {{ suffix }} + assert: + that: + - script_info.scripts is defined + - script_info.scripts is sequence() + quiet: yes + +- name: Assert that the correct number of scripts are returned {{ suffix }} + assert: + that: script_info.scripts | length >= expected_scripts | length + fail_msg: >- + Expected {{ expected_scripts | length }} scripts, got {{ script_info.scripts | map(attribute='name') | join(',') }} ({{ script_info.scripts | length}}) + quiet: yes + +- name: Assert that all expected scripts are present {{ suffix }} + assert: + that: item in (script_info.scripts | map(attribute='name')) + fail_msg: "Expected script '{{ item }}' not found in results." + quiet: yes + loop: "{{ expected_scripts }}" + loop_control: + label: "Assert '{{ item }}' in result." + +- include_tasks: contains_all_fields.yml + vars: + dict_to_check: "{{ item }}" + loop: "{{ + only_check_first + | default(True) + | bool + | ternary([ script_info.scripts[0] ], script_info.scripts) + }}" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/tasks/contains_all_fields.yml b/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/tasks/contains_all_fields.yml new file mode 100644 index 000000000..20750e003 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/tasks/contains_all_fields.yml @@ -0,0 +1,13 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: Check for key ('{{ key }}') in result + assert: + that: key in dict_to_check + quiet: yes + fail_msg: "'{{ key }}' not found in dict." + loop_control: + loop_var: key + loop: "{{ expected_fields }}" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/tasks/main.yml new file mode 100644 index 000000000..b3f1394e5 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/tasks/main.yml @@ -0,0 +1,49 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: Reset repositories + ansible.windows.win_shell: | + Get-PSRepository | Unregister-PSRepository + Register-PSRepository -Default + +- name: Set up directory repository + ansible.windows.win_copy: + src: "{{ role_path }}/files/" + dest: "{{ repo_path }}" + force: no + +- name: Register repository + community.windows.win_psrepository: + name: "{{ repository_name }}" + source_location: "{{ repo_path }}" + installation_policy: trusted + +- block: + - name: Install Scripts + community.windows.win_psscript: + name: "{{ item }}" + state: latest + repository: "{{ repository_name }}" + loop: "{{ sample_scripts }}" + + - name: Run Tests + import_tasks: tests.yml + + - name: Run Tests (check mode) + import_tasks: tests.yml + vars: + run_check_mode: True + + always: + - name: Remove Scripts + community.windows.win_psscript: + name: "{{ item }}" + state: absent + loop: "{{ sample_scripts }}" + + - name: Reset repositories + ansible.windows.win_shell: | + Get-PSRepository | Unregister-PSRepository + Register-PSRepository -Default diff --git a/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/tasks/tests.yml new file mode 100644 index 000000000..45931c567 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_psscript_info/tasks/tests.yml @@ -0,0 +1,92 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: Get info on a named script + vars: + expected_scripts: + - "{{ sample_scripts[0] }}" + block: + - name: Get single script {{ suffix }} + community.windows.win_psscript_info: + name: "{{ expected_scripts[0] }}" + register: script_info + + - include_tasks: common.yml + + # block + check_mode: "{{ run_check_mode }}" + +- name: Get info on scripts by wildcard match + vars: + wildcard: "Install-*" + expected_scripts: "{{ + sample_scripts + | select('match', '^Install-') + | list + }}" + block: + - name: Get multiple scripts by wildcard {{ suffix }} + community.windows.win_psscript_info: + name: "{{ wildcard }}" + register: script_info + + - include_tasks: common.yml + + # block + check_mode: "{{ run_check_mode }}" + +- name: Get info on scripts by repository + vars: + repository: "{{ repository_name }}" + expected_scripts: "{{ sample_scripts }}" + block: + - name: Get multiple scripts by repository {{ suffix }} + community.windows.win_psscript_info: + repository: "{{ repository }}" + register: script_info + + - include_tasks: common.yml + + # block + check_mode: "{{ run_check_mode }}" + +- name: Get info on scripts by repository and name pattern + vars: + wildcard: "*RPC" + repository: "{{ repository_name }}" + expected_scripts: "{{ + sample_scripts + | select('match', 'RPC$') + | list + }}" + block: + - name: Get scripts by wildcard and repository {{ suffix }} + community.windows.win_psscript_info: + name: "{{ wildcard }}" + repository: "{{ repository }}" + register: script_info + + - include_tasks: common.yml + + # block + check_mode: "{{ run_check_mode }}" + +- name: Get info on all scripts + vars: + expected_scripts: "{{ sample_scripts }}" + block: + - name: Get all scripts {{ suffix }} + community.windows.win_psscript_info: + register: script_info + + - include_tasks: common.yml + + # one last time checking all the fields + - include_tasks: common.yml + vars: + only_check_first: False + + # block + check_mode: "{{ run_check_mode }}" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_pssession_configuration/aliases b/ansible_collections/community/windows/tests/integration/targets/win_pssession_configuration/aliases new file mode 100644 index 000000000..3cf5b97e8 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_pssession_configuration/aliases @@ -0,0 +1 @@ +shippable/windows/group3 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_pssession_configuration/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_pssession_configuration/defaults/main.yml new file mode 100644 index 000000000..3a6642d34 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_pssession_configuration/defaults/main.yml @@ -0,0 +1,3 @@ +--- +config_name: Test +config_description: A config for testing diff --git a/ansible_collections/community/windows/tests/integration/targets/win_pssession_configuration/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_pssession_configuration/meta/main.yml new file mode 100644 index 000000000..5648cebe8 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_pssession_configuration/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + # - setup_remote_tmp_dir diff --git a/ansible_collections/community/windows/tests/integration/targets/win_pssession_configuration/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_pssession_configuration/tasks/main.yml new file mode 100644 index 000000000..a56930b5d --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_pssession_configuration/tasks/main.yml @@ -0,0 +1,21 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- block: + - ansible.windows.setup: + gather_subset: + - '!all' + - '!min' + - powershell_version + + - import_tasks: tests.yml + + always: + - name: Remove + ansible.windows.win_shell: | + Unregister-PSSessionConfiguration -Name '{{ config_name }}' -Force -ErrorAction SilentlyContinue + ignore_errors: yes + # sometimes this fails because the connection was interrupted + # it doesn't matter because it's still successful diff --git a/ansible_collections/community/windows/tests/integration/targets/win_pssession_configuration/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_pssession_configuration/tasks/tests.yml new file mode 100644 index 000000000..23e62164f --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_pssession_configuration/tasks/tests.yml @@ -0,0 +1,448 @@ +# This file is part of Ansible + +# Copyright: (c) 2020, Brian Scholer <@briantist> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: Create a simple configuration (check) + win_pssession_configuration: + name: "{{ config_name }}" + description: "{{ config_description }}" + modules_to_import: Microsoft.PowerShell.Utility + register: status + check_mode: yes + +- name: Check for changed status + assert: + that: status is changed + quiet: yes + +- name: Create a simple configuration + win_pssession_configuration: + name: "{{ config_name }}" + description: "{{ config_description }}" + modules_to_import: Microsoft.PowerShell.Utility + register: status + +- name: Check for changed status + assert: + that: status is changed + quiet: yes + +- name: Create a simple configuration again (check) + win_pssession_configuration: + name: "{{ config_name }}" + description: "{{ config_description }}" + modules_to_import: Microsoft.PowerShell.Utility + register: status + check_mode: yes + +- name: Check for unchanged status + assert: + that: status is not changed + quiet: yes + +- name: Create a simple configuration again + win_pssession_configuration: + name: "{{ config_name }}" + description: "{{ config_description }}" + modules_to_import: Microsoft.PowerShell.Utility + register: status + +- name: Check for unchanged status + assert: + that: status is not changed + quiet: yes + +####### + +- name: Try to disable polling (failure expected) + win_pssession_configuration: + name: "{{ config_name }}" + description: "{{ config_description }}~~~~" + modules_to_import: Microsoft.PowerShell.Utility + async_timeout: 500 + async_poll: 0 + register: status + ignore_errors: yes + +- name: Ensure it failed + assert: + that: + - status is failed + - status.msg is search('async_poll') + quiet: yes + +- name: Try it with the keywords (failure expected) + win_pssession_configuration: + name: "{{ config_name }}" + description: "{{ config_description }}" + modules_to_import: Microsoft.PowerShell.Utility + async: 500 + poll: 2 + register: status + ignore_errors: yes + +- name: Check for expected failire + assert: + that: status is failed + quiet: yes + +####### + +- name: Try to set a parameter that's only available in PowerShell 5+ (check) + win_pssession_configuration: + name: "{{ config_name }}" + description: "{{ config_description }}" + mount_user_drive: True + register: status + check_mode: yes + ignore_errors: yes + +- name: Check for version-based status + assert: + that: (status is failed) == (ansible_powershell_version < 5) + quiet: yes + +####### +# Incomplete tests for future enhancement of waiting for connections before making changes + +# - name: Start a 45 second WinRM connection +# ansible.windows.win_shell: | +# Invoke-Command -ComputerName . -ScriptBlock { Start-Sleep -Seconds 45 } -AsJob + +# - name: Make a change with a 10 second fail on timeout (failure expected) +# win_pssession_configuration: +# name: "{{ config_name }}" +# description: "{{ config_description }}" +# mount_user_drive: False +# existing_connection_timeout_seconds: 10 +# existing_connection_timeout_action: fail +# register: status +# ignore_errors: yes + +# - name: Check for failed status +# assert: +# that: status is failed +# quiet: yes + +# - name: Start a 25 second WinRM connection +# ansible.windows.win_shell: | +# Invoke-Command -ComputerName . -ScriptBlock { Start-Sleep -Seconds 25 } -AsJob + +# - name: Make a change with a 45 second fail on timeout (success expected) +# win_pssession_configuration: +# name: "{{ config_name }}" +# description: "{{ config_description }}" +# mount_user_drive: False +# existing_connection_timeout_seconds: 10 +# existing_connection_timeout_action: fail +# register: status + +# - name: Check for changed status +# assert: +# that: status is changed +# quiet: yes + +####### +- name: Omit a field that doesn't matter (check) + win_pssession_configuration: + name: "{{ config_name }}" + # description: "{{ config_description }}" # intentionally omitted + modules_to_import: Microsoft.PowerShell.Utility + register: status + check_mode: yes + +- name: Check for unchanged status + assert: + that: status is not changed + quiet: yes + +- name: Omit a field that doesn't matter + win_pssession_configuration: + name: "{{ config_name }}" + # description: "{{ config_description }}" # intentionally omitted + modules_to_import: Microsoft.PowerShell.Utility + register: status + +- name: Check for unchanged status + assert: + that: status is not changed + quiet: yes + +####### + +- name: Omit a field that doesn't matter by default but does explicitly (check) + win_pssession_configuration: + name: "{{ config_name }}" + # description: "{{ config_description }}" # intentionally omitted + modules_to_import: Microsoft.PowerShell.Utility + lenient_config_fields: + - guid + - author + register: status + check_mode: yes + +- name: Check for changed status + assert: + that: status is changed + quiet: yes + +- name: Omit a field that doesn't matter by default but does explicitly + win_pssession_configuration: + name: "{{ config_name }}" + # description: "{{ config_description }}" # intentionally omitted + modules_to_import: Microsoft.PowerShell.Utility + lenient_config_fields: + - guid + - author + register: status + +- name: Check for changed status + assert: + that: status is changed + quiet: yes + +######## + +- name: Change something not in the config file (check) + win_pssession_configuration: + name: "{{ config_name }}" + thread_options: reuse_thread + use_shared_process: True + register: status + check_mode: yes + +- name: Check for changed status + assert: + that: status is changed + quiet: yes + +- name: Change something not in the config file + win_pssession_configuration: + name: "{{ config_name }}" + thread_options: reuse_thread + use_shared_process: True + register: status + +- name: Check for changed status + assert: + that: status is changed + quiet: yes + +- name: Change something not in the config file (again) (check) + win_pssession_configuration: + name: "{{ config_name }}" + thread_options: reuse_thread + use_shared_process: True + register: status + check_mode: yes + +- name: Check for unchanged status + assert: + that: status is not changed + quiet: yes + +- name: Change something not in the config file (again) + win_pssession_configuration: + name: "{{ config_name }}" + thread_options: reuse_thread + use_shared_process: True + register: status + +- name: Check for unchanged status + assert: + that: status is not changed + quiet: yes + +###### + +- name: Check custom type parameters for PowerShell <= 4 (check) + win_pssession_configuration: + name: "{{ config_name }}" + guid: "{{ 'test' | to_uuid }}" + powershell_version: '3.0' + maximum_received_data_size_per_command_mb: 5.0E+11 + maximum_received_object_size_mb: 6.6666666666666666666666666 + register: status + when: ansible_powershell_version <= 4 + +- name: Check custom type parameters for PowerShell >= 5 (check) + win_pssession_configuration: + name: "{{ config_name }}" + guid: "{{ 'test' | to_uuid }}" + powershell_version: '3.0' + user_drive_maximum_size: 5000000000 + maximum_received_data_size_per_command_mb: 5.0E+11 + maximum_received_object_size_mb: 6.6666666666666666666666666 + register: status + when: ansible_powershell_version >= 5 + +###### + +- name: Reset back to a steady state + win_pssession_configuration: + name: "{{ config_name }}" + register: status + +- name: Check for changed status + assert: + that: status is changed + quiet: yes + ignore_errors: yes + +- name: Reset back to a steady state (again) + win_pssession_configuration: + name: "{{ config_name }}" + register: status + +- name: Check for unchanged status + assert: + that: status is not changed + quiet: yes + ignore_errors: yes + +####### + +- name: "RunAs Tests" + vars: + runas_user: "{{ ansible_user }}" + runas_pass: "{{ ansible_password }}" + block: + - name: Change runas credential (check) + win_pssession_configuration: + name: "{{ config_name }}" + run_as_credential_username: "{{ runas_user }}" + run_as_credential_password: "{{ runas_pass }}" + register: status + check_mode: yes + + - name: Check for changed status + assert: + that: status is changed + quiet: yes + + - name: Change runas credential + win_pssession_configuration: + name: "{{ config_name }}" + run_as_credential_username: "{{ runas_user }}" + run_as_credential_password: "{{ runas_pass }}" + register: status + + - name: Check for changed status + assert: + that: status is changed + quiet: yes + + - name: Try an invalid runas credential (failure expected) + win_pssession_configuration: + name: "{{ config_name }}" + run_as_credential_username: Fake + run_as_credential_password: Fake + register: status + ignore_errors: yes + + - name: Check for failed status + assert: + that: status is failed + quiet: yes + + - name: Ensure the sessions configuration still exists with the working user + ansible.windows.win_shell: | + $sc = Get-PSSessionConfiguration -Name '{{ config_name }}' + if ($sc.RunAsUser -ne '{{ runas_user }}') { + throw + } + register: status + +####### + +- name: complex configuration for pwsh 5.1+ + when: ansible_powershell_version > 4 + block: + - name: Create a complex configuration + win_pssession_configuration: + name: "{{ config_name }}" + description: "{{ config_description }}" + modules_to_import: Microsoft.PowerShell.Utility + visible_aliases: [] + visible_cmdlets: [] + visible_external_commands: [] + visible_functions: [] + security_descriptor_sddl: "O:NSG:BAD:P(A;;GA;;;IU)(A;;GA;;;BA)(A;;GA;;;RM)S:P(AU;FA;GA;;;WD)(AU;SA;GXGW;;;WD)" + register: status + + - name: Check for changed status + assert: + that: status is changed + quiet: yes + + - name: Create a complex configuration again (check) + win_pssession_configuration: + name: "{{ config_name }}" + description: "{{ config_description }}" + modules_to_import: Microsoft.PowerShell.Utility + visible_aliases: [] + visible_cmdlets: [] + visible_external_commands: [] + visible_functions: [] + security_descriptor_sddl: "O:NSG:BAD:P(A;;GA;;;IU)(A;;GA;;;BA)(A;;GA;;;RM)S:P(AU;FA;GA;;;WD)(AU;SA;GXGW;;;WD)" + register: status + check_mode: yes + + - name: Check for unchanged status + assert: + that: status is not changed + quiet: yes + + - name: Create a complex configuration again + win_pssession_configuration: + name: "{{ config_name }}" + description: "{{ config_description }}" + modules_to_import: Microsoft.PowerShell.Utility + visible_aliases: [] + visible_cmdlets: [] + visible_external_commands: [] + visible_functions: [] + security_descriptor_sddl: "O:NSG:BAD:P(A;;GA;;;IU)(A;;GA;;;BA)(A;;GA;;;RM)S:P(AU;FA;GA;;;WD)(AU;SA;GXGW;;;WD)" + register: status + + - name: Check for unchanged status + assert: + that: status is not changed + quiet: yes + +####### + +- name: Test session configuration removal (check) + win_pssession_configuration: + name: "{{ config_name }}" + state: absent + register: status + check_mode: yes + +- name: Check for changed status + assert: + that: status is changed + quiet: yes + +- name: Test session configuration removal + win_pssession_configuration: + name: "{{ config_name }}" + state: absent + register: status + +- name: Check for changed status + assert: + that: status is changed + quiet: yes + +- name: Test session configuration removal (again) + win_pssession_configuration: + name: "{{ config_name }}" + state: absent + register: status + +- name: Check for unchanged status + assert: + that: status is not changed + quiet: yes diff --git a/ansible_collections/community/windows/tests/integration/targets/win_rabbitmq_plugin/aliases b/ansible_collections/community/windows/tests/integration/targets/win_rabbitmq_plugin/aliases new file mode 100644 index 000000000..aefcbeb1b --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_rabbitmq_plugin/aliases @@ -0,0 +1,2 @@ +shippable/windows/group3 +disabled diff --git a/ansible_collections/community/windows/tests/integration/targets/win_rabbitmq_plugin/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_rabbitmq_plugin/tasks/main.yml new file mode 100644 index 000000000..ce60dd109 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_rabbitmq_plugin/tasks/main.yml @@ -0,0 +1,7 @@ +# Setup action creates ansible_distribution_version variable +- ansible.windows.setup: + +- include_tasks: tasks/tests.yml + # Works on windows >= Windows 7/Windows Server 2008 R2 + # See https://github.com/ansible/ansible/pull/28118#issuecomment-323684042 for additional info. + when: ansible_distribution_version is version('6.1', '>=') diff --git a/ansible_collections/community/windows/tests/integration/targets/win_rabbitmq_plugin/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_rabbitmq_plugin/tasks/tests.yml new file mode 100644 index 000000000..53ee0efdf --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_rabbitmq_plugin/tasks/tests.yml @@ -0,0 +1,134 @@ +- name: Ensure RabbitMQ installed + chocolatey.chocolatey.win_chocolatey: + name: rabbitmq + state: present + +- name: Ensure that rabbitmq_management plugin disabled + win_rabbitmq_plugin: + names: rabbitmq_management + state: disabled + +- name: Enable rabbitmq_management plugin in check mode + win_rabbitmq_plugin: + names: rabbitmq_management + state: enabled + check_mode: yes + register: enable_plugin_in_check_mode + +- name: Check that enabling plugin in check mode succeeds with a change + assert: + that: + - enable_plugin_in_check_mode.changed == true + +- name: Enable rabbitmq_management plugin in check mode again + win_rabbitmq_plugin: + names: rabbitmq_management + state: enabled + check_mode: yes + register: enable_plugin_in_check_mode_again + +- name: Check that enabling plugin in check mode does not make changes + assert: + that: + - enable_plugin_in_check_mode_again.changed == true + +- name: Enable rabbitmq_management plugin + win_rabbitmq_plugin: + names: rabbitmq_management + state: enabled + register: enable_plugin + +- name: Check that enabling plugin succeeds with a change + assert: + that: + - enable_plugin.changed == true + - enable_plugin.enabled == ['rabbitmq_management'] + +- name: Enable enabled rabbitmq_management plugin + win_rabbitmq_plugin: + names: rabbitmq_management + state: enabled + register: enable_plugin_again + +- name: Check that enabling enabled plugin succeeds without a change + assert: + that: + - enable_plugin_again.changed == false + - enable_plugin_again.enabled == [] + +- name: Enable new plugin when 'new_only' option is 'no' (by default) and there are installed plugins + win_rabbitmq_plugin: + names: rabbitmq_mqtt + state: enabled + check_mode: yes + register: enable_plugin_without_new_only + +- name: Check that 'new_only == no' option enables new plugin and disables the old one + assert: + that: + - enable_plugin_without_new_only.changed == true + - enable_plugin_without_new_only.enabled == ['rabbitmq_mqtt'] + - enable_plugin_without_new_only.disabled == ['rabbitmq_management'] + +- name: Enable new plugin when 'new_only' option is 'yes' and there are installed plugins + win_rabbitmq_plugin: + names: rabbitmq_mqtt + state: enabled + new_only: yes + check_mode: yes + register: enable_plugin_with_new_only + +- name: Check that 'new_only == yes' option just enables new plugin + assert: + that: + - enable_plugin_with_new_only.changed == true + - enable_plugin_with_new_only.enabled == ['rabbitmq_mqtt'] + - enable_plugin_with_new_only.disabled == [] + +- name: Disable rabbitmq_management plugin in check mode + win_rabbitmq_plugin: + names: rabbitmq_management + state: disabled + check_mode: yes + register: disable_plugin_in_check_mode + +- name: Check that disabling plugin in check mode succeeds with a change + assert: + that: + - disable_plugin_in_check_mode.changed == true + +- name: Disable rabbitmq_management plugin in check mode again + win_rabbitmq_plugin: + names: rabbitmq_management + state: disabled + check_mode: yes + register: disable_plugin_in_check_mode_again + +- name: Check that disabling plugin in check mode does not make changes + assert: + that: + - disable_plugin_in_check_mode_again.changed == true + +- name: Disable rabbitmq_management plugin + win_rabbitmq_plugin: + names: rabbitmq_management + state: disabled + register: disable_plugin + +- name: Check that disabling plugin succeeds with a change + assert: + that: + - disable_plugin.changed == true + - disable_plugin.disabled == ['rabbitmq_management'] + +- name: Disable disabled rabbitmq_management plugin + win_rabbitmq_plugin: + names: rabbitmq_management + state: disabled + register: disable_plugin_again + +- name: Check that disabling disabled plugin succeeds without a change + assert: + that: + - disable_plugin_again.changed == false + - disable_plugin_again.disabled == [] diff --git a/ansible_collections/community/windows/tests/integration/targets/win_rds/aliases b/ansible_collections/community/windows/tests/integration/targets/win_rds/aliases new file mode 100644 index 000000000..8d0a829ba --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_rds/aliases @@ -0,0 +1,5 @@ +shippable/windows/group3 +destructive +win_rds_cap +win_rds_rap +win_rds_settings diff --git a/ansible_collections/community/windows/tests/integration/targets/win_rds/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_rds/defaults/main.yml new file mode 100644 index 000000000..a332469ea --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_rds/defaults/main.yml @@ -0,0 +1,9 @@ +# win_rds_cap +test_win_rds_cap_name: Ansible Test CAP + +# win_rds_rap +test_win_rds_rap_name: Ansible Test RAP + +# win_rds_settings +test_win_rds_settings_path: '{{ remote_tmp_dir }}\win_rds_settings' +rds_cert_suject: rdg.test.com diff --git a/ansible_collections/community/windows/tests/integration/targets/win_rds/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_rds/meta/main.yml new file mode 100644 index 000000000..45806c8dc --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_rds/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_remote_tmp_dir
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_rds/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_rds/tasks/main.yml new file mode 100644 index 000000000..25b32a553 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_rds/tasks/main.yml @@ -0,0 +1,73 @@ +--- +# Cannot use win_feature to install RDS on Server 2008 +- name: check if feature is availble + ansible.windows.win_shell: if (Get-Command -Name Add-WindowsFeature -ErrorAction SilentlyContinue) { $true } else { $false } + changed_when: False + register: module_available + +- name: install Remote Desktop Gateway features + when: module_available.stdout | trim | bool + block: + - name: ensure Remote Desktop Gateway services are installed + ansible.windows.win_feature: + name: + - RDS-Gateway + - RDS-Licensing + - RDS-RD-Server + state: present + register: rds_install + + - name: reboot server if needed + ansible.windows.win_reboot: + when: rds_install.reboot_required + + # After a reboot Windows is still configuring the feature, this is a hack to wait until that is finished + - name: wait for component servicing to be finished + ansible.windows.win_shell: | + $start = Get-Date + $path = "HKLM:\SYSTEM\CurrentControlSet\Control\Winlogon\Notifications\Components\TrustedInstaller" + $tries = 0 + while ((Get-ItemProperty -Path $path -Name Events).Events.Contains("CreateSession")) { + $tries += 1 + Start-Sleep -Seconds 5 + if (((Get-Date) - $start).TotalSeconds -gt 180) { + break + } + } + $tries + changed_when: False + + - name: run win_rds_cap integration tests + include_tasks: win_rds_cap.yml + + - name: run win_rds_rap integration tests + include_tasks: win_rds_rap.yml + + - name: run win_rds_settings integration tests + include_tasks: win_rds_settings.yml + + always: + # Server 2008 R2 requires us to remove this first before the other features + - name: remove the RDS-Gateway feature + ansible.windows.win_feature: + name: RDS-Gateway + state: absent + register: rds_uninstall + + - name: reboot after removing RDS-Gateway feature + ansible.windows.win_reboot: + when: rds_uninstall.reboot_required + + # Now remove the remaining features + - name: remove installed RDS feature + ansible.windows.win_feature: + name: + - RDS-Licensing + - RDS-RD-Server + - Web-Server # not part of the initial feature install but RDS-Gateway requires this and it breaks httptester + state: absent + register: rds_uninstall2 + + - name: reboot after feature removal + ansible.windows.win_reboot: + when: rds_uninstall2.reboot_required diff --git a/ansible_collections/community/windows/tests/integration/targets/win_rds/tasks/win_rds_cap.yml b/ansible_collections/community/windows/tests/integration/targets/win_rds/tasks/win_rds_cap.yml new file mode 100644 index 000000000..24adb25b9 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_rds/tasks/win_rds_cap.yml @@ -0,0 +1,9 @@ +--- +- name: run tests with cleanup + block: + - name: run tests + include_tasks: win_rds_cap_tests.yml + + always: + - name: delete all CAPs + ansible.windows.win_shell: Import-Module RemoteDesktopServices; Remove-Item -Path RDS:\GatewayServer\CAP\* -Recurse diff --git a/ansible_collections/community/windows/tests/integration/targets/win_rds/tasks/win_rds_cap_tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_rds/tasks/win_rds_cap_tests.yml new file mode 100644 index 000000000..a7dc059a2 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_rds/tasks/win_rds_cap_tests.yml @@ -0,0 +1,264 @@ +--- +- name: test create a new CAP (check mode) + win_rds_cap: + name: '{{ test_win_rds_cap_name }}' + user_groups: + - administrators + - users@builtin + state: present + register: new_cap_check + check_mode: yes + +- name: get result of create a new CAP (check mode) + ansible.windows.win_shell: Import-Module RemoteDesktopServices; Write-Host (Test-Path "RDS:\GatewayServer\CAP\{{ test_win_rds_cap_name }}") + register: new_cap_actual_check + +- name: assert results of create a new CAP (check mode) + assert: + that: + - new_cap_check.changed == true + - new_cap_actual_check.stdout_lines[0] == "False" + +- name: test create a new CAP + win_rds_cap: + name: '{{ test_win_rds_cap_name }}' + user_groups: + - administrators + - users@builtin + state: present + register: new_cap + +- name: get result of create a new CAP + ansible.windows.win_shell: Import-Module RemoteDesktopServices; Write-Host (Test-Path "RDS:\GatewayServer\CAP\{{ test_win_rds_cap_name }}") + register: new_cap_actual + +- name: assert results of create a new CAP + assert: + that: + - new_cap.changed == true + - new_cap_actual.stdout_lines[0] == "True" + +- name: test create a new CAP (idempotent) + win_rds_cap: + name: '{{ test_win_rds_cap_name }}' + user_groups: + - administrators + - users@builtin + state: present + register: new_cap_again + +- name: get result of create a new CAP (idempotent) + ansible.windows.win_shell: Import-Module RemoteDesktopServices; Write-Host (Test-Path "RDS:\GatewayServer\CAP\{{ test_win_rds_cap_name }}") + register: new_cap_actual_again + +- name: assert results of create a new CAP (idempotent) + assert: + that: + - new_cap_again.changed == false + - new_cap_actual_again.stdout_lines[0] == "True" + +- name: test edit a CAP + win_rds_cap: + name: '{{ test_win_rds_cap_name }}' + user_groups: + # Test with different group name formats + - users@builtin + - .\guests + computer_groups: + - administrators + auth_method: both + session_timeout: 20 + session_timeout_action: reauth + allow_only_sdrts_servers: true + idle_timeout: 10 + redirect_clipboard: false + redirect_drives: false + redirect_printers: false + redirect_serial: false + redirect_pnp: false + state: disabled + register: edit_cap + +- name: get result of edit a CAP + ansible.windows.win_shell: | + Import-Module RemoteDesktopServices; + $cap_path = "RDS:\GatewayServer\CAP\{{ test_win_rds_cap_name }}" + $cap = @{} + Get-ChildItem -Path "$cap_path" | foreach { $cap.Add($_.Name,$_.CurrentValue) } + $cap.DeviceRedirection = @{} + Get-ChildItem -Path "$cap_path\DeviceRedirection" | foreach { $cap.DeviceRedirection.Add($_.Name, ($_.CurrentValue -eq 1)) } + $cap.UserGroups = @(Get-ChildItem -Path "$cap_path\UserGroups" | Select -ExpandProperty Name) + $cap.ComputerGroups = @(Get-ChildItem -Path "$cap_path\ComputerGroups" | Select -ExpandProperty Name) + $cap | ConvertTo-Json + register: edit_cap_actual_json + +- name: parse result of edit a CAP. + set_fact: + edit_cap_actual: '{{ edit_cap_actual_json.stdout | from_json }}' + +- name: assert results of edit a CAP + assert: + that: + - edit_cap.changed == true + - edit_cap_actual.Status == "0" + - edit_cap_actual.EvaluationOrder == "1" + - edit_cap_actual.AllowOnlySDRTSServers == "1" + - edit_cap_actual.AuthMethod == "3" + - edit_cap_actual.IdleTimeout == "10" + - edit_cap_actual.SessionTimeoutAction == "1" + - edit_cap_actual.SessionTimeout == "20" + - edit_cap_actual.DeviceRedirection.Clipboard == false + - edit_cap_actual.DeviceRedirection.DiskDrives == false + - edit_cap_actual.DeviceRedirection.PlugAndPlayDevices == false + - edit_cap_actual.DeviceRedirection.Printers == false + - edit_cap_actual.DeviceRedirection.SerialPorts == false + - edit_cap_actual.UserGroups | length == 2 + - edit_cap_actual.UserGroups[0] == "Users@BUILTIN" + - edit_cap_actual.UserGroups[1] == "Guests@BUILTIN" + - edit_cap_actual.ComputerGroups | length == 1 + - edit_cap_actual.ComputerGroups[0] == "Administrators@BUILTIN" + +- name: test remove all computer groups of CAP + win_rds_cap: + name: '{{ test_win_rds_cap_name }}' + computer_groups: [] + register: remove_computer_groups_cap + +- name: get result of remove all computer groups of CAP + ansible.windows.win_shell: | + Import-Module RemoteDesktopServices; + $cap_path = "RDS:\GatewayServer\CAP\{{ test_win_rds_cap_name }}" + Write-Host @(Get-ChildItem -Path "$cap_path\ComputerGroups" | Select -ExpandProperty Name).Count + register: remove_computer_groups_cap_actual + +- name: assert results of remove all computer groups of CAP + assert: + that: + - remove_computer_groups_cap.changed == true + - remove_computer_groups_cap_actual.stdout_lines[0] == "0" + +- name: test create a CAP in second position + win_rds_cap: + name: '{{ test_win_rds_cap_name }} Second' + user_groups: + - users@builtin + order: 2 + state: present + register: second_cap + +- name: get result of create a CAP in second position + ansible.windows.win_shell: Import-Module RemoteDesktopServices; Write-Host (Get-Item "RDS:\GatewayServer\CAP\{{ test_win_rds_cap_name }} Second\EvaluationOrder").CurrentValue + register: second_cap_actual + +- name: assert results of create a CAP in second position + assert: + that: + - second_cap.changed == true + - second_cap.warnings is not defined + - second_cap_actual.stdout_lines[0] == "2" + +- name: test create a CAP with order greater than existing CAP count + win_rds_cap: + name: '{{ test_win_rds_cap_name }} Last' + user_groups: + - users@builtin + order: 50 + state: present + register: cap_big_order + +- name: get result of create a CAP with order greater than existing CAP count + ansible.windows.win_shell: Import-Module RemoteDesktopServices; Write-Host (Get-Item "RDS:\GatewayServer\CAP\{{ test_win_rds_cap_name }} Last\EvaluationOrder").CurrentValue + register: cap_big_order_actual + +- name: assert results of create a CAP with order greater than existing CAP count + assert: + that: + - cap_big_order.changed == true + - cap_big_order.warnings | length == 1 + - cap_big_order_actual.stdout_lines[0] == "3" + +- name: test remove CAP (check mode) + win_rds_cap: + name: '{{ test_win_rds_cap_name }}' + state: absent + register: remove_cap_check + check_mode: yes + +- name: get result of remove CAP (check mode) + ansible.windows.win_shell: Import-Module RemoteDesktopServices; Write-Host (Test-Path "RDS:\GatewayServer\CAP\{{ test_win_rds_cap_name }}") + register: remove_cap_actual_check + +- name: assert results of remove CAP (check mode) + assert: + that: + - remove_cap_check.changed == true + - remove_cap_actual_check.stdout_lines[0] == "True" + +- name: test remove CAP + win_rds_cap: + name: '{{ test_win_rds_cap_name }}' + state: absent + register: remove_cap_check + +- name: get result of remove CAP + ansible.windows.win_shell: Import-Module RemoteDesktopServices; Write-Host (Test-Path "RDS:\GatewayServer\CAP\{{ test_win_rds_cap_name }}") + register: remove_cap_actual_check + +- name: assert results of remove CAP + assert: + that: + - remove_cap_check.changed == true + - remove_cap_actual_check.stdout_lines[0] == "False" + +- name: test remove CAP (idempotent) + win_rds_cap: + name: '{{ test_win_rds_cap_name }}' + state: absent + register: remove_cap_check + +- name: get result of remove CAP (idempotent) + ansible.windows.win_shell: Import-Module RemoteDesktopServices; Write-Host (Test-Path "RDS:\GatewayServer\CAP\{{ test_win_rds_cap_name }}") + register: remove_cap_actual_check + +- name: assert results of remove CAP (idempotent) + assert: + that: + - remove_cap_check.changed == false + - remove_cap_actual_check.stdout_lines[0] == "False" + +- name: fail when create a new CAP without user group + win_rds_cap: + name: '{{ test_win_rds_cap_name }}' + state: present + register: new_cap_without_group + check_mode: yes + failed_when: "new_cap_without_group.msg != 'User groups must be defined to create a new CAP.'" + +- name: fail when create a new CAP with an empty user group list + win_rds_cap: + name: '{{ test_win_rds_cap_name }}' + user_groups: [] + state: present + register: new_cap_empty_group_list + check_mode: yes + failed_when: "new_cap_empty_group_list.msg is not search('cannot be an empty list')" + +- name: fail when create a new CAP with an invalid user group + win_rds_cap: + name: '{{ test_win_rds_cap_name }}' + user_groups: + - fake_group + state: present + register: new_cap_invalid_user_group + check_mode: yes + failed_when: new_cap_invalid_user_group.changed != false or new_cap_invalid_user_group.msg is not search('is not a valid account') + +- name: fail when create a new CAP with an invalid computer group + win_rds_cap: + name: '{{ test_win_rds_cap_name }}' + computer_groups: + - fake_group + state: present + register: new_cap_invalid_computer_group + check_mode: yes + failed_when: new_cap_invalid_computer_group.changed != false or new_cap_invalid_computer_group.msg is not search('is not a valid account') diff --git a/ansible_collections/community/windows/tests/integration/targets/win_rds/tasks/win_rds_rap.yml b/ansible_collections/community/windows/tests/integration/targets/win_rds/tasks/win_rds_rap.yml new file mode 100644 index 000000000..774076269 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_rds/tasks/win_rds_rap.yml @@ -0,0 +1,9 @@ +--- +- name: run tests with cleanup + block: + - name: run tests + include_tasks: win_rds_rap_tests.yml + + always: + - name: delete all RAPs + ansible.windows.win_shell: Import-Module RemoteDesktopServices; Remove-Item -Path RDS:\GatewayServer\RAP\* -Recurse diff --git a/ansible_collections/community/windows/tests/integration/targets/win_rds/tasks/win_rds_rap_tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_rds/tasks/win_rds_rap_tests.yml new file mode 100644 index 000000000..1e0fdbe03 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_rds/tasks/win_rds_rap_tests.yml @@ -0,0 +1,254 @@ +--- +- name: test create a new RAP (check mode) + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + user_groups: + - administrators + - users@builtin + state: present + register: new_rap_check + check_mode: yes + +- name: get result of create a new RAP (check mode) + ansible.windows.win_shell: Import-Module RemoteDesktopServices; Write-Host (Test-Path "RDS:\GatewayServer\RAP\{{ test_win_rds_rap_name }}") + register: new_rap_actual_check + +- name: assert results of create a new RAP (check mode) + assert: + that: + - new_rap_check.changed == true + - new_rap_actual_check.stdout_lines[0] == "False" + +- name: test create a new RAP + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + user_groups: + - administrators + - users@builtin + state: present + register: new_rap + +- name: get result of create a new RAP + ansible.windows.win_shell: Import-Module RemoteDesktopServices; Write-Host (Test-Path "RDS:\GatewayServer\RAP\{{ test_win_rds_rap_name }}") + register: new_rap_actual + +- name: assert results of create a new RAP + assert: + that: + - new_rap.changed == true + - new_rap_actual.stdout_lines[0] == "True" + +- name: test create a new RAP (idempotent) + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + user_groups: + - administrators + - users@builtin + state: present + register: new_rap_again + +- name: get result of create a new RAP (idempotent) + ansible.windows.win_shell: Import-Module RemoteDesktopServices; Write-Host (Test-Path "RDS:\GatewayServer\RAP\{{ test_win_rds_rap_name }}") + register: new_rap_actual_again + +- name: assert results of create a new RAP (idempotent) + assert: + that: + - new_rap_again.changed == false + - new_rap_actual_again.stdout_lines[0] == "True" + +- name: test edit a RAP + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + description: 'Description of {{ test_win_rds_rap_name }}' + user_groups: + # Test with different group name formats + - users@builtin + - .\guests + computer_group_type: ad_network_resource_group + computer_group: administrators + allowed_ports: + - 3389 + - 3390 + - 3391 + state: disabled + register: edit_rap + +- name: get result of edit a RAP + ansible.windows.win_shell: | + Import-Module RemoteDesktopServices; + $rap_path = "RDS:\GatewayServer\RAP\{{ test_win_rds_rap_name }}" + $rap = @{} + Get-ChildItem -Path "$rap_path" | foreach { $rap.Add($_.Name,$_.CurrentValue) } + $rap.UserGroups = @(Get-ChildItem -Path "$rap_path\UserGroups" | Select -ExpandProperty Name) + $rap | ConvertTo-Json + register: edit_rap_actual_json + +- name: parse result of edit a RAP. + set_fact: + edit_rap_actual: '{{ edit_rap_actual_json.stdout | from_json }}' + +- name: assert results of edit a RAP + assert: + that: + - edit_rap.changed == true + - edit_rap_actual.Status == "0" + - edit_rap_actual.Description == "Description of {{ test_win_rds_rap_name }}" + - edit_rap_actual.PortNumbers == "3389,3390,3391" + - edit_rap_actual.UserGroups | length == 2 + - edit_rap_actual.UserGroups[0] == "Users@BUILTIN" + - edit_rap_actual.UserGroups[1] == "Guests@BUILTIN" + - edit_rap_actual.ComputerGroupType == "1" + - edit_rap_actual.ComputerGroup == "Administrators@BUILTIN" + +- name: test edit a RAP (indempotent) + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + description: 'Description of {{ test_win_rds_rap_name }}' + user_groups: + - users@builtin + - guests@builtin + computer_group_type: ad_network_resource_group + computer_group: Administrators@BUILTIN + allowed_ports: + - 3389 + - 3390 + - 3391 + state: disabled + register: edit_rap_again + +- name: assert results of edit a RAP (indempotent) + assert: + that: + - edit_rap_again.changed == false + +- name: test allow all ports + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + allowed_ports: any + register: edit_rap_allow_all_ports + +- name: get result of allow all ports + ansible.windows.win_shell: Import-Module RemoteDesktopServices; Write-Host (Get-Item "RDS:\GatewayServer\RAP\{{ test_win_rds_rap_name }}\PortNumbers").CurrentValue + register: edit_rap_allow_all_ports_actual + +- name: assert results of allow all ports + assert: + that: + - edit_rap_allow_all_ports.changed == true + - edit_rap_allow_all_ports_actual.stdout_lines[0] == "*" + +- name: test remove RAP (check mode) + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + state: absent + register: remove_rap_check + check_mode: yes + +- name: get result of remove RAP (check mode) + ansible.windows.win_shell: Import-Module RemoteDesktopServices; Write-Host (Test-Path "RDS:\GatewayServer\RAP\{{ test_win_rds_rap_name }}") + register: remove_rap_actual_check + +- name: assert results of remove RAP (check mode) + assert: + that: + - remove_rap_check.changed == true + - remove_rap_actual_check.stdout_lines[0] == "True" + +- name: test remove RAP + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + state: absent + register: remove_rap + +- name: get result of remove RAP + ansible.windows.win_shell: Import-Module RemoteDesktopServices; Write-Host (Test-Path "RDS:\GatewayServer\RAP\{{ test_win_rds_rap_name }}") + register: remove_rap_actual + +- name: assert results of remove RAP + assert: + that: + - remove_rap.changed == true + - remove_rap_actual.stdout_lines[0] == "False" + +- name: test remove RAP (idempotent) + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + state: absent + register: remove_rap_again + +- name: get result of remove RAP (idempotent) + ansible.windows.win_shell: Import-Module RemoteDesktopServices; Write-Host (Test-Path "RDS:\GatewayServer\RAP\{{ test_win_rds_rap_name }}") + register: remove_rap_actual_again + +- name: assert results of remove RAP (idempotent) + assert: + that: + - remove_rap_again.changed == false + - remove_rap_actual_again.stdout_lines[0] == "False" + +- name: fail when create a new RAP without user group + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + state: present + register: new_rap_without_group + check_mode: yes + failed_when: "new_rap_without_group.msg != 'User groups must be defined to create a new RAP.'" + +- name: fail when create a new RAP with an empty user group list + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + user_groups: [] + state: present + register: new_rap_empty_group_list + check_mode: yes + failed_when: "new_rap_empty_group_list.msg is not search('cannot be an empty list')" + +- name: fail when create a new RAP with an invalid user group + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + user_groups: + - fake_group + state: present + register: new_rap_invalid_group + check_mode: yes + failed_when: new_rap_invalid_group.changed != false or new_rap_invalid_group.msg is not search('is not a valid account') + +- name: fail when create a new RAP with an invalid AD computer group + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + user_groups: + - administrators + computer_group_type: ad_network_resource_group + computer_group: fake_ad_group + state: present + register: new_rap_invalid_ad_computer_group + check_mode: yes + failed_when: new_rap_invalid_ad_computer_group.changed != false or new_rap_invalid_ad_computer_group.msg is not search('is not a valid account') + +- name: fail when create a new RAP with an invalid gateway managed computer group + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + user_groups: + - administrators + computer_group_type: rdg_group + computer_group: fake_rdg_group + state: present + register: new_rap_invalid_rdg_computer_group + check_mode: yes + failed_when: new_rap_invalid_rdg_computer_group.changed != false or new_rap_invalid_rdg_computer_group.msg is not search('is not a valid gateway managed computer group') + +- name: fail when create a new RAP with invalid port numbers + win_rds_rap: + name: '{{ test_win_rds_rap_name }}' + user_groups: + - administrators + allowed_ports: + - '{{ item }}' + state: present + loop: + - invalid_port_number + - 65536 + register: new_rap_invalid_port + check_mode: yes + failed_when: new_rap_invalid_port.changed != false or new_rap_invalid_port.msg is not search('is not a valid port number') diff --git a/ansible_collections/community/windows/tests/integration/targets/win_rds/tasks/win_rds_settings.yml b/ansible_collections/community/windows/tests/integration/targets/win_rds/tasks/win_rds_settings.yml new file mode 100644 index 000000000..dff44963c --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_rds/tasks/win_rds_settings.yml @@ -0,0 +1,66 @@ +- name: run tests with cleanup + block: + - name: gather facts + ansible.windows.setup: + filter: ansible_hostname + + - name: ensure testing folders exists + ansible.windows.win_file: + path: '{{test_win_rds_settings_path}}' + state: directory + + - name: deploy test artifacts + ansible.windows.win_template: + src: '{{item}}.j2' + dest: '{{test_win_rds_settings_path}}\{{item | basename}}' + with_items: + - rds_base_cfg.xml + + - name: import RDS test configuration + ansible.windows.win_shell: | + $ts = Get-WmiObject Win32_TSGatewayServer -namespace root\cimv2\TerminalServices + $import_xml = Get-Content {{test_win_rds_settings_path}}\rds_base_cfg.xml + $import_result = $ts.Import(45, $import_xml) + exit $import_result.ReturnValue + + - name: write certreq file + ansible.windows.win_copy: + content: |- + [NewRequest] + Subject = "CN={{ rds_cert_suject }}" + KeyLength = 2048 + KeyAlgorithm = RSA + MachineKeySet = true + RequestType = Cert + KeyUsage = 0xA0 ; Digital Signature, Key Encipherment + [EnhancedKeyUsageExtension] + OID=1.3.6.1.5.5.7.3.1 ; Server Authentication + dest: '{{test_win_rds_settings_path}}\certreq.txt' + + - name: create self signed cert from certreq + ansible.windows.win_command: certreq -new -machine {{test_win_rds_settings_path}}\certreq.txt {{test_win_rds_settings_path}}\certreqresp.txt + + - name: register certificate thumbprint + raw: '(gci Cert:\LocalMachine\my | ? {$_.subject -eq "CN={{ rds_cert_suject }}"})[0].Thumbprint' + register: rds_cert_thumbprint + + - name: run tests + include_tasks: win_rds_settings_tests.yml + + always: + - name: restore RDS base configuration + ansible.windows.win_shell: | + $ts = Get-WmiObject Win32_TSGatewayServer -namespace root\cimv2\TerminalServices + $import_xml = Get-Content {{test_win_rds_settings_path}}\rds_base_cfg.xml + $import_result = $ts.Import(45, $import_xml) + exit $import_result.ReturnValue + + - name: remove certificate + raw: 'remove-item cert:\localmachine\my\{{ item }} -force -ea silentlycontinue' + with_items: + - "{{ rds_cert_thumbprint.stdout_lines[0] }}" + + - name: cleanup test artifacts + ansible.windows.win_file: + path: '{{test_win_rds_settings_path}}' + state: absent diff --git a/ansible_collections/community/windows/tests/integration/targets/win_rds/tasks/win_rds_settings_tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_rds/tasks/win_rds_settings_tests.yml new file mode 100644 index 000000000..0b611eb95 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_rds/tasks/win_rds_settings_tests.yml @@ -0,0 +1,89 @@ +--- +- name: test change RDS settings (check mode) + win_rds_settings: + max_connections: 50 + certificate_hash: '{{rds_cert_thumbprint.stdout_lines[0]}}' + ssl_bridging: https_https + enable_only_messaging_capable_clients: yes + register: configure_rds_check + check_mode: yes + +- name: get result of change RDS settings (check mode) + ansible.windows.win_shell: | + Import-Module RemoteDesktopServices + (Get-Item RDS:\GatewayServer\MaxConnections).CurrentValue + (Get-Item RDS:\GatewayServer\SSLCertificate\Thumbprint).CurrentValue + (Get-Item RDS:\GatewayServer\SSLBridging).CurrentValue + (Get-Item RDS:\GatewayServer\EnableOnlyMessagingCapableClients).CurrentValue + register: configure_rds_actual_check + +- name: assert results of change RDS settings (check mode) + assert: + that: + - configure_rds_check.changed == true + - configure_rds_actual_check.stdout_lines[0] != "50" + - configure_rds_actual_check.stdout_lines[1] != rds_cert_thumbprint.stdout_lines[0] + - configure_rds_actual_check.stdout_lines[2] == "0" + - configure_rds_actual_check.stdout_lines[3] == "0" + +- name: test change RDS settings + win_rds_settings: + max_connections: 50 + certificate_hash: '{{rds_cert_thumbprint.stdout_lines[0]}}' + ssl_bridging: https_https + enable_only_messaging_capable_clients: yes + register: configure_rds + +- name: get result of change RDS settings + ansible.windows.win_shell: | + Import-Module RemoteDesktopServices + (Get-Item RDS:\GatewayServer\MaxConnections).CurrentValue + (Get-Item RDS:\GatewayServer\SSLCertificate\Thumbprint).CurrentValue + (Get-Item RDS:\GatewayServer\SSLBridging).CurrentValue + (Get-Item RDS:\GatewayServer\EnableOnlyMessagingCapableClients).CurrentValue + register: configure_rds_actual + +- name: assert results of change RDS settings + assert: + that: + - configure_rds.changed == true + - configure_rds_actual.stdout_lines[0] == "50" + - configure_rds_actual.stdout_lines[1] == rds_cert_thumbprint.stdout_lines[0] + - configure_rds_actual.stdout_lines[2] == "2" + - configure_rds_actual.stdout_lines[3] == "1" + +- name: test change RDS settings (idempotent) + win_rds_settings: + max_connections: 50 + certificate_hash: '{{rds_cert_thumbprint.stdout_lines[0]}}' + ssl_bridging: https_https + enable_only_messaging_capable_clients: yes + register: configure_rds_again + +- name: assert results of change RDS settings (idempotent) + assert: + that: + - configure_rds_again.changed == false + +- name: test disable connection limit + win_rds_settings: + max_connections: -1 + register: disable_limit + +- name: get result of disable connection limit + ansible.windows.win_shell: | + Import-Module RemoteDesktopServices + (Get-Item RDS:\GatewayServer\MaxConnections).CurrentValue -eq (Get-Item RDS:\GatewayServer\MaxConnectionsAllowed).CurrentValue + register: disable_limit_actual + +- name: assert results of disable connection limit + assert: + that: + - disable_limit.changed == true + - disable_limit_actual.stdout_lines[0] == "True" + +- name: fail with invalid certificate thumbprint + win_rds_settings: + certificate_hash: 72E8BD0216FA14100192A3E8B7B150C65B4B0817 + register: fail_invalid_cert + failed_when: fail_invalid_cert.msg is not search('Unable to locate certificate')
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_rds/templates/rds_base_cfg.xml.j2 b/ansible_collections/community/windows/tests/integration/targets/win_rds/templates/rds_base_cfg.xml.j2 new file mode 100644 index 000000000..5aa48ed84 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_rds/templates/rds_base_cfg.xml.j2 @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-16"?> +<?TSGateway version="1.0"?> +<TsgServer> + <ServerName>{{ ansible_hostname }}</ServerName> + <ServerSettings> + <MaxConnections>4294967295</MaxConnections> + <UnlimitedConnections>1</UnlimitedConnections> + <CentralCapEnabled>0</CentralCapEnabled> + <RequestSOH>0</RequestSOH> + <OnlyConsentCapableClients>0</OnlyConsentCapableClients> + <LogEvents> + <LogEvent> + <Name>LogChannelDisconnect</Name> + <Enabled>1</Enabled> + </LogEvent> + <LogEvent> + <Name>LogFailureChannelConnect</Name> + <Enabled>1</Enabled> + </LogEvent> + <LogEvent> + <Name>LogFailureConnectionAuthorizationCheck</Name> + <Enabled>1</Enabled> + </LogEvent> + <LogEvent> + <Name>LogFailureResourceAuthorizationCheck</Name> + <Enabled>1</Enabled> + </LogEvent> + <LogEvent> + <Name>LogSuccessfulChannelConnect</Name> + <Enabled>1</Enabled> + </LogEvent> + <LogEvent> + <Name>LogSuccessfulConnectionAuthorizationCheck</Name> + <Enabled>1</Enabled> + </LogEvent> + <LogEvent> + <Name>LogSuccessfulResourceAuthorizationCheck</Name> + <Enabled>1</Enabled> + </LogEvent> + </LogEvents> + <AuthenticationPlugin>native</AuthenticationPlugin> + <AuthorizationPlugin>native</AuthorizationPlugin> + <ConsentMessageText/> + <AdminMessageText/> + <AdminMsgStartDate/> + <AdminMsgEndDate/> + <SslBridging>0</SslBridging> + <HttpIPAddress>*</HttpIPAddress> + <UdpIPAddress>*</UdpIPAddress> + <HttpPort>443</HttpPort> + <UdpPort>3391</UdpPort> + <IsUdpEnabled>1</IsUdpEnabled> + <EnforceChannelBinding>1</EnforceChannelBinding> + </ServerSettings> + <Caps/> + <Raps/> + <ResourceGroups/> +</TsgServer>
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_region/aliases b/ansible_collections/community/windows/tests/integration/targets/win_region/aliases new file mode 100644 index 000000000..3cf5b97e8 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_region/aliases @@ -0,0 +1 @@ +shippable/windows/group3 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_region/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_region/tasks/main.yml new file mode 100644 index 000000000..be2b8be21 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_region/tasks/main.yml @@ -0,0 +1,252 @@ +# test code for the win_region module +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +- name: expect failure when only setting copy_settings + win_region: + copy_settings: False + register: actual + failed_when: actual.msg != "An argument for 'format', 'location' or 'unicode_language' needs to be supplied" + +- name: expect failure when using invalid geo id for the location + win_region: + location: 111111 + register: actual + failed_when: actual.msg != "The argument location '111111' does not contain a valid Geo ID" + +- name: expect failure when using invalid culture for format + win_region: + format: ab-CD + register: actual + failed_when: actual.msg != "The argument format 'ab-CD' does not contain a valid Culture Name" + +- name: expect failure when using invalid culture for unicode_language + win_region: + unicode_language: ab-CD + register: actual + failed_when: actual.msg != "The argument unicode_language 'ab-CD' does not contain a valid Culture Name" + +- name: set settings all to English Australia before tests for a baseline + win_region: + location: 12 + format: en-AU + unicode_language: en-AU + +- name: reboot server to set properties + ansible.windows.win_reboot: + +- name: check that changing location in check mode works + win_region: + location: 244 + register: check_location + check_mode: yes + +- name: get current location value + ansible.windows.win_command: powershell (Get-ItemProperty -Path 'HKCU:\Control Panel\International\Geo').Nation + register: actual_location + +- name: check assertion about location change in check mode + assert: + that: + - "actual_location.stdout_lines[0] == '12'" # Corresponds to en-AU + - "check_location is changed" + - "check_location.restart_required == False" + +- name: set location to United States + win_region: + location: 244 + register: location + +- name: get current location value + ansible.windows.win_command: powershell (Get-ItemProperty -Path 'HKCU:\Control Panel\International\Geo').Nation + register: actual_location + +- name: check assertion about location change + assert: + that: + - "actual_location.stdout_lines[0] == '244'" # Corresponds to en-US + - "location is changed" + - "location.restart_required == False" + +- name: set location to United States again + win_region: + location: 244 + register: location_again + +- name: check that the result did not change + assert: + that: + - "location_again is not changed" + - "location_again.restart_required == False" + +- name: set format to English United States in check mode + win_region: + format: en-US + register: check_format + check_mode: yes + +- name: get actual format value from check mode + ansible.windows.win_command: powershell (Get-Culture).Name + register: actual_format + +- name: check assertion about location change in check mode + assert: + that: + - "actual_format.stdout_lines[0] == 'en-AU'" + - "check_format is changed" + - "check_format.restart_required == False" + +- name: set format to English United States + win_region: + format: en-US + register: format + +- name: get actual format value + ansible.windows.win_command: powershell (Get-Culture).Name + register: actual_format + +- name: check assertion about format change + assert: + that: + - "actual_format.stdout_lines[0] == 'en-US'" + - "format is changed" + - "format.restart_required == False" + +- name: set format to English United States again + win_region: + format: en-US + register: format_again + +- name: check that the result did not change + assert: + that: + - "format_again is not changed" + - "format_again.restart_required == False" + +- name: set unicode_language to English United States in check mode + win_region: + unicode_language: en-US + register: check_unicode + check_mode: yes + +- name: get actual unicode values + ansible.windows.win_command: powershell (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Nls\Language').Default + register: actual_unicode + +- name: check assertion about unicode language change in check mode + assert: + that: + - "actual_unicode.stdout_lines[0] == '0c09'" + - "check_unicode is changed" + - "check_unicode.restart_required == True" + +- name: set unicode_language to English United States + win_region: + unicode_language: en-US + register: unicode + +- name: reboot the server after changing unicode language + ansible.windows.win_reboot: + when: unicode.restart_required + +- name: get actual unicode value + ansible.windows.win_command: powershell (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Nls\Language').Default + register: actual_unicode + +- name: check assertion about unicode language change + assert: + that: + - "actual_unicode.stdout_lines[0] == '0409'" # corresponds to en-US + - "unicode is changed" + - "unicode.restart_required == True" + +- name: set unicode_language to English United States again + win_region: + unicode_language: en-US + register: unicode_again + +- name: check that the result did not change + assert: + that: + - "unicode_again is not changed" + - "unicode_again.restart_required == False" + +- name: copy settings when setting to the same format check mode + win_region: + format: en-US + copy_settings: True + register: check_copy_same + check_mode: yes + +- name: check that the result did not change in check mode + assert: + that: + - "check_copy_same is not changed" + - "check_copy_same.restart_required == False" + +- name: copy settings when setting to the same format + win_region: + format: en-US + copy_settings: True + register: copy_same + +- name: check that the result did not change + assert: + that: + - "copy_same is not changed" + - "copy_same.restart_required == False" + +- name: copy setting when setting to a different format + win_region: + format: en-GB + copy_settings: True + register: copy + +- name: get actual format value after copy_settings + ansible.windows.win_command: powershell (Get-Culture).Name + register: actual_copy + +- name: get locale name for local service registry hive + ansible.windows.win_command: powershell "New-PSDrive -Name HKU -PSProvider Registry -Root Registry::HKEY_USERS | Out-Null; (Get-ItemProperty 'HKU:\S-1-5-19\Control Panel\International').LocaleName" + register: actual_local + +- name: get locale name for network service registry hive + ansible.windows.win_command: powershell "New-PSDrive -Name HKU -PSProvider Registry -Root Registry::HKEY_USERS | Out-Null; (Get-ItemProperty 'HKU:\S-1-5-20\Control Panel\International').LocaleName" + register: actual_network + +- name: load temp hive + ansible.windows.win_command: reg load HKU\TEMP C:\Users\Default\NTUSER.DAT + +- name: get locale name for default registry hive + ansible.windows.win_command: powershell "New-PSDrive -Name HKU -PSProvider Registry -Root Registry::HKEY_USERS | Out-Null; (Get-ItemProperty 'HKU:\TEMP\Control Panel\International').LocaleName" + register: actual_temp + +- name: unload temp hive + ansible.windows.win_command: reg unload HKU\TEMP + +- name: get locale name for default registry hive + ansible.windows.win_command: powershell "New-PSDrive -Name HKU -PSProvider Registry -Root Registry::HKEY_USERS | Out-Null; (Get-ItemProperty 'HKU:\.DEFAULT\Control Panel\International').LocaleName" + register: actual_default + +- name: check assertions about copy setting when setting to a different format + assert: + that: + - "actual_copy.stdout_lines[0] == 'en-GB'" + - "actual_local.stdout_lines[0] == 'en-GB'" + - "actual_network.stdout_lines[0] == 'en-GB'" + - "actual_temp.stdout_lines[0] == 'en-GB'" + - "actual_default.stdout_lines[0] == 'en-GB'" + - "copy is changed" + - "copy.restart_required == False" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_regmerge/aliases b/ansible_collections/community/windows/tests/integration/targets/win_regmerge/aliases new file mode 100644 index 000000000..423ce3910 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_regmerge/aliases @@ -0,0 +1 @@ +shippable/windows/group2 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_regmerge/files/settings1.reg b/ansible_collections/community/windows/tests/integration/targets/win_regmerge/files/settings1.reg Binary files differnew file mode 100644 index 000000000..baec75b2a --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_regmerge/files/settings1.reg diff --git a/ansible_collections/community/windows/tests/integration/targets/win_regmerge/files/settings2.reg b/ansible_collections/community/windows/tests/integration/targets/win_regmerge/files/settings2.reg Binary files differnew file mode 100644 index 000000000..fc2612cb8 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_regmerge/files/settings2.reg diff --git a/ansible_collections/community/windows/tests/integration/targets/win_regmerge/files/settings3.reg b/ansible_collections/community/windows/tests/integration/targets/win_regmerge/files/settings3.reg Binary files differnew file mode 100644 index 000000000..fbe7411c9 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_regmerge/files/settings3.reg diff --git a/ansible_collections/community/windows/tests/integration/targets/win_regmerge/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_regmerge/meta/main.yml new file mode 100644 index 000000000..9f37e96cd --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_regmerge/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_remote_tmp_dir diff --git a/ansible_collections/community/windows/tests/integration/targets/win_regmerge/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_regmerge/tasks/main.yml new file mode 100644 index 000000000..af640eb93 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_regmerge/tasks/main.yml @@ -0,0 +1,133 @@ +# test code for the win_regmerge module +# (c) 2014, Michael DeHaan <michael.dehaan@gmail.com> + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +# clear the area of the registry we are using for tests +- name: remove setting + ansible.windows.win_regedit: + key: 'HKLM:\SOFTWARE\Wow6432Node\Cow Corp' + state: absent + +# copy over some registry files to work with +- name: copy over some registry files to work with + ansible.windows.win_copy: src={{item}} dest={{ remote_tmp_dir }}\\{{item}} + with_items: + - settings1.reg + - settings2.reg + - settings3.reg + +# test 1 - basic test of changed behaviour +# merge in REG_SZ +- name: test 1 merge in a setting + win_regmerge: + path: "{{ remote_tmp_dir }}\\settings1.reg" + register: merge11_result + +- assert: + that: + - "merge11_result.changed == true" + +# re run the merge +- name: test 1 merge in the setting again + win_regmerge: + path: "{{ remote_tmp_dir }}\\settings1.reg" + register: merge12_result + +# without a compare to key, should always report changed +- assert: + that: + - "merge12_result.changed == true" +# assert changed false + +# prune reg key +- name: test 1 remove setting + ansible.windows.win_regedit: + key: 'HKLM:\SOFTWARE\Wow6432Node\Cow Corp' + state: absent + +# +# test 2, observe behaviour when compare_to param is set +# +- name: test 2 merge in a setting + win_regmerge: + path: "{{ remote_tmp_dir }}\\settings1.reg" + compare_to: 'HKLM:\SOFTWARE\Wow6432Node\Cow Corp\Moosic\ILikeToMooveIt' + register: merge21_result + +- assert: + that: + - "merge21_result.changed == true" + +# re run the merge +- name: test 2 merge in the setting again but with compare_key + win_regmerge: + path: "{{ remote_tmp_dir }}\\settings1.reg" + compare_to: 'HKLM:\SOFTWARE\Wow6432Node\Cow Corp\Moosic\ILikeToMooveIt' + register: merge22_result + +# with a compare to key, should now report not changed +- assert: + that: + - "merge22_result.changed == false" +# assert changed false + +# prune the contents of the registry from the parent of the compare key downwards +- name: test 2 clean up remove setting + ansible.windows.win_regedit: + key: 'HKLM:\SOFTWARE\Wow6432Node\Cow Corp' + state: absent + +# test 3 merge in more complex settings +- name: test 3 merge in a setting + win_regmerge: + path: "{{ remote_tmp_dir }}\\settings3.reg" + compare_to: 'HKLM:\SOFTWARE\Wow6432Node\Cow Corp\Moo Monitor' + register: merge31_result + +- assert: + that: + - "merge31_result.changed == true" + +# re run the merge +- name: test 3 merge in the setting again but with compare_key check + win_regmerge: + path: "{{ remote_tmp_dir }}\\settings3.reg" + compare_to: 'HKLM:\SOFTWARE\Wow6432Node\Cow Corp\Moo Monitor' + register: merge32_result + +# with a compare to key, should now report not changed +- assert: + that: + - "merge32_result.changed == false" +# assert changed false + +# prune the contents of the registry from the compare key downwards +- name: test 3 clean up remove setting + ansible.windows.win_regedit: + key: 'HKLM:\SOFTWARE\Wow6432Node\Cow Corp' + state: absent + +# clean up registry files + +- name: clean up registry files + ansible.windows.win_file: path={{ remote_tmp_dir }}\\{{item}} state=absent + with_items: + - settings1.reg + - settings2.reg + - settings3.reg + +# END OF win_regmerge tests diff --git a/ansible_collections/community/windows/tests/integration/targets/win_regmerge/templates/win_line_ending.j2 b/ansible_collections/community/windows/tests/integration/targets/win_regmerge/templates/win_line_ending.j2 new file mode 100644 index 000000000..d0cefd76f --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_regmerge/templates/win_line_ending.j2 @@ -0,0 +1,4 @@ +#jinja2: newline_sequence:'\r\n' +{{ templated_var }}
+{{ templated_var }}
+{{ templated_var }}
diff --git a/ansible_collections/community/windows/tests/integration/targets/win_regmerge/vars/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_regmerge/vars/main.yml new file mode 100644 index 000000000..1e8f64ccf --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_regmerge/vars/main.yml @@ -0,0 +1 @@ +templated_var: templated_var_loaded diff --git a/ansible_collections/community/windows/tests/integration/targets/win_route/aliases b/ansible_collections/community/windows/tests/integration/targets/win_route/aliases new file mode 100644 index 000000000..4f4664b68 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_route/aliases @@ -0,0 +1 @@ +shippable/windows/group5 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_route/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_route/defaults/main.yml new file mode 100644 index 000000000..a77ebc16c --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_route/defaults/main.yml @@ -0,0 +1,3 @@ +--- +default_gateway: 192.168.1.1 +destination_ip_address: 192.168.2.10 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_route/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_route/tasks/main.yml new file mode 100644 index 000000000..4bbdee5b5 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_route/tasks/main.yml @@ -0,0 +1,29 @@ +--- +# test code for the win_psmodule module when using winrm connection +# (c) 2017, Daniele Lazzari <lazzari@mailup.com> + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + + +- name: get os info + ansible.windows.win_shell: '[Environment]::OSVersion.Version -ge [Version]"6.3"' + register: os + +- name: Perform with os Windows 2012R2 or newer + when: os.stdout_lines[0] == "True" + block: + - name: run all tasks + include: tests.yml diff --git a/ansible_collections/community/windows/tests/integration/targets/win_route/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_route/tasks/tests.yml new file mode 100644 index 000000000..dc456068a --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_route/tasks/tests.yml @@ -0,0 +1,79 @@ +--- +- name: add a static route + win_route: + destination: "{{ destination_ip_address }}/32" + gateway: "{{ default_gateway }}" + metric: 1 + state: present + register: route + +- name: check if route successfully added + ansible.windows.win_shell: (Get-CimInstance win32_ip4PersistedrouteTable -Filter "Destination = '{{ destination_ip_address }}'").Caption + register: route_added + +- name: check route default gateway + ansible.windows.win_shell: (Get-CimInstance win32_ip4PersistedrouteTable -Filter "Destination = '{{ destination_ip_address }}'").NextHop + register: route_gateway + +- name: test if route successfully added + assert: + that: + - route is changed + - route_added.stdout_lines[0] == "{{ destination_ip_address }}" + - route_gateway.stdout_lines[0] == "{{ default_gateway }}" + +- name: add a static route to test idempotency + win_route: + destination: "{{ destination_ip_address }}/32" + gateway: "{{ default_gateway }}" + metric: 1 + state: present + register: idempotent_route + +- name: test idempotency + assert: + that: + - idempotent_route is not changed + - idempotent_route.output == "Static route already exists" + +- name: remove route + win_route: + destination: "{{ destination_ip_address }}/32" + state: absent + register: route_removed + +- name: check route is removed + ansible.windows.win_shell: Get-CimInstance win32_ip4PersistedrouteTable -Filter "Destination = '{{ destination_ip_address }}'" + register: check_route_removed + +- name: test route is removed + assert: + that: + - route_removed is changed + - check_route_removed.stdout == '' + +- name: remove static route to test idempotency + win_route: + destination: "{{ destination_ip_address }}/32" + state: absent + register: idempotent_route_removed + +- name: test idempotency + assert: + that: + - idempotent_route_removed is not changed + - idempotent_route_removed.output == "No route to remove" + +- name: add route to wrong ip address + win_route: + destination: "715.18.0.0/32" + gateway: "{{ default_gateway }}" + metric: 1 + state: present + ignore_errors: yes + register: wrong_ip + +- name: test route to wrong ip address + assert: + that: + - wrong_ip is failed diff --git a/ansible_collections/community/windows/tests/integration/targets/win_say/aliases b/ansible_collections/community/windows/tests/integration/targets/win_say/aliases new file mode 100644 index 000000000..4cd27b3cb --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_say/aliases @@ -0,0 +1 @@ +shippable/windows/group1 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_say/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_say/tasks/main.yml new file mode 100644 index 000000000..5b37a19ba --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_say/tasks/main.yml @@ -0,0 +1,44 @@ +# CI hosts don't have a valid Speech package so we rely on check mode for basic +# sanity tests +--- +- name: Warn of impending deployment + win_say: + msg: Warning, deployment commencing in 5 minutes, please log out. + check_mode: "{{ win_say_check_mode |default('yes') }}" + +- name: Using a specified voice and a start sound + win_say: + msg: Warning, deployment commencing in 5 minutes, please log out. + start_sound_path: C:\Windows\Media\ding.wav + voice: Microsoft Hazel Desktop + check_mode: "{{ win_say_check_mode |default('yes') }}" + +- name: Example with start and end sound + win_say: + msg: New software installed + start_sound_path: C:\Windows\Media\Windows Balloon.wav + end_sound_path: C:\Windows\Media\chimes.wav + check_mode: "{{ win_say_check_mode |default('yes') }}" + +- name: Create message file + ansible.windows.win_copy: + content: Stay calm and carry on + dest: C:\Windows\Temp\win_say_message.txt + +- name: Text from file example + win_say: + msg_file: C:\Windows\Temp\win_say_message.txt + start_sound_path: C:\Windows\Media\Windows Balloon.wav + end_sound_path: C:\Windows\Media\chimes.wav + check_mode: "{{ win_say_check_mode |default('yes') }}" + +- name: Remove message file + ansible.windows.win_file: + path: C:\Windows\Temp\win_say_message.txt + state: absent + +- name: Different speech speed + win_say: + speech_speed: 5 + msg: Stay calm and proceed to the closest fire exit. + check_mode: "{{ win_say_check_mode |default('yes') }}" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task/aliases b/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task/aliases new file mode 100644 index 000000000..4f4664b68 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task/aliases @@ -0,0 +1 @@ +shippable/windows/group5 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task/defaults/main.yml new file mode 100644 index 000000000..f6cd77da3 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task/defaults/main.yml @@ -0,0 +1,15 @@ +--- +test_scheduled_task_name: Ansible Test +test_scheduled_task_path: \Ansible Test Folder +test_scheduled_task_user: MooCow +test_scheduled_task_pass: Password01 + +test_scheduled_task_invalid_chars: + - '\' + - '/' + - ':' + - '*' + - '"' + - '<' + - '>' + - '|' diff --git a/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task/tasks/clean.yml b/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task/tasks/clean.yml new file mode 100644 index 000000000..df7bbd06e --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task/tasks/clean.yml @@ -0,0 +1,16 @@ +# cleans up each test to ensure a blank slate +--- +- win_scheduled_task: + name: '{{item.name}}' + path: '{{item.path|default(omit)}}' + state: absent + with_items: + - name: Task # old tests + path: \Path + - name: '{{test_scheduled_task_name}}' + - name: '{{test_scheduled_task_name}}' + path: '{{test_scheduled_task_path}}' + +- ansible.windows.win_user: + name: '{{test_scheduled_task_user}}' + state: absent diff --git a/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task/tasks/failures.yml b/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task/tasks/failures.yml new file mode 100644 index 000000000..57c4e4895 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task/tasks/failures.yml @@ -0,0 +1,161 @@ +# test out the known failure cases to ensure we have decent error messages +--- +# ensure we have good error messages for invalid task chars +- name: fail to create tasks with invalid characters + win_scheduled_task: + name: "Bad {{ item }} {{ test_scheduled_task_name }}" + state: present + actions: + - path: cmd.exe + arguments: /c echo hi + loop: "{{ test_scheduled_task_invalid_chars }}" + register: fail_invalid_chars + failed_when: fail_invalid_chars is not failed or fail_invalid_chars.msg is not search('The following characters are not valid') + +- name: fail create task without an action + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + register: fail_create_without_action + failed_when: fail_create_without_action.msg != 'cannot create a task with no actions, set at least one action with a path to an executable' + +- name: fail both username and group are set + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: '{{ansible_user}}' + group: '{{ansible_user}}' + register: fail_username_and_group + failed_when: fail_username_and_group.msg != 'username and group can not be set at the same time' + +- name: fail logon type s4u but no password set + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + logon_type: s4u + register: fail_lt_s4u_not_set + failed_when: fail_lt_s4u_not_set.msg != 'password must be set when logon_type=s4u' + +- name: fail logon type group but no group set + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + logon_type: group + register: fail_lt_group_not_set + failed_when: fail_lt_group_not_set.msg != 'group must be set when logon_type=group' + +- name: fail logon type service but non service user set + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + logon_type: service_account + username: '{{ansible_user}}' + register: fail_lt_service_invalid_user + failed_when: fail_lt_service_invalid_user.msg != 'username must be SYSTEM, LOCAL SERVICE or NETWORK SERVICE when logon_type=service_account' + +- name: fail trigger with no type + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + triggers: + - delay: test + register: fail_trigger_no_type + failed_when: fail_trigger_no_type.msg != "a trigger entry must contain a key 'type' with a value of 'event', 'time', 'daily', 'weekly', 'monthly', 'monthlydow', 'idle', 'registration', 'boot', 'logon', 'session_state_change'" + +- name: fail trigger with datetime in incorrect format + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + triggers: + - type: time + start_boundary: fake + register: fail_trigger_invalid_datetime + failed_when: fail_trigger_invalid_datetime.msg != "trigger option 'start_boundary' must be in the format 'YYYY-MM-DDThh:mm:ss' format but was 'fake'" + +- name: fail trigger with duration in incorrect format + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + triggers: + - type: boot + execution_time_limit: fake + register: fail_trigger_invalid_duration + failed_when: fail_trigger_invalid_duration.msg != "trigger option 'execution_time_limit' must be in the XML duration format but was 'fake'" + +- name: fail trigger option invalid day of the week + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + triggers: + - type: weekly + start_boundary: '2000-01-01T00:00:01' + days_of_week: fakeday + register: fail_trigger_invalid_day_of_week + failed_when: fail_trigger_invalid_day_of_week.msg != "invalid day of week 'fakeday', check the spelling matches the full day name" + +- name: fail trigger option invalid day of the month + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + triggers: + - type: monthly + start_boundary: '2000-01-01T00:00:01' + days_of_month: 35 + register: fail_trigger_invalid_day_of_month + failed_when: fail_trigger_invalid_day_of_month.msg != "invalid day of month '35', please specify numbers from 1-31" + +- name: fail trigger option invalid week of the month + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + triggers: + - type: monthlydow + start_boundary: '2000-01-01T00:00:01' + weeks_of_month: 5 + register: fail_trigger_invalid_week_of_month + failed_when: fail_trigger_invalid_week_of_month.msg != "invalid week of month '5', please specify weeks from 1-4" + +- name: fail trigger option invalid month of the year + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + triggers: + - type: monthlydow + start_boundary: '2000-01-01T00:00:01' + months_of_year: fakemonth + register: fail_trigger_invalid_month_of_year + failed_when: fail_trigger_invalid_month_of_year.msg != "invalid month name 'fakemonth', please specify full month name" + +- name: fail trigger repetition with duration in incorrect format + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + triggers: + - type: boot + repetition: + - duration: fake + register: fail_trigger_repetition_invalid_duration + failed_when: fail_trigger_repetition_invalid_duration.msg != "trigger option 'duration' must be in the XML duration format but was 'fake'" + +- name: fail trigger repetition with interval in incorrect format + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + triggers: + - type: boot + repetition: + - interval: fake + register: fail_trigger_repetition_invalid_interval + failed_when: fail_trigger_repetition_invalid_interval.msg != "trigger option 'interval' must be in the XML duration format but was 'fake'" + +- name: fail trigger repetition option interval greater than duration + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + triggers: + - type: boot + repetition: + - interval: PT5M + duration: PT1M + register: fail_trigger_repetition_interval_greater_than_duration + failed_when: fail_trigger_repetition_interval_greater_than_duration.msg != "trigger repetition option 'interval' value 'PT5M' must be less than or equal to 'duration' value 'PT1M'" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task/tasks/main.yml new file mode 100644 index 000000000..5142ac9ab --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task/tasks/main.yml @@ -0,0 +1,24 @@ +--- +- name: remove test tasks before test + include_tasks: clean.yml + +- block: + - name: Test failure scenarios + include_tasks: failures.yml + + - name: Test normal scenarios + include_tasks: tests.yml + + - include_tasks: clean.yml + + - name: Test principals + include_tasks: principals.yml + + - include_tasks: clean.yml + + - name: Test triggers + include_tasks: triggers.yml + + always: + - name: remove test tasks after test + include_tasks: clean.yml diff --git a/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task/tasks/principals.yml b/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task/tasks/principals.yml new file mode 100644 index 000000000..9961855f8 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task/tasks/principals.yml @@ -0,0 +1,436 @@ +--- +- name: create test user + ansible.windows.win_user: + name: '{{test_scheduled_task_user}}' + password: '{{test_scheduled_task_pass}}' + state: present + groups: + - Administrators + +- name: task with password principal (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: '{{test_scheduled_task_user}}' + password: '{{test_scheduled_task_pass}}' + logon_type: password + update_password: no + actions: + - path: cmd.exe + register: task_with_password_check + check_mode: yes + +- name: get result of task with password principal (check mode) + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: task_with_password_result_check + +- name: assert results of task with password principal (check mode) + assert: + that: + - task_with_password_check is changed + - task_with_password_result_check.task_exists == False + +- name: task with password principal + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: '{{test_scheduled_task_user}}' + password: '{{test_scheduled_task_pass}}' + logon_type: password + update_password: no + actions: + - path: cmd.exe + register: task_with_password + +- name: get result of task with password principal + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: task_with_password_result + +- name: assert results of task with password principal + assert: + that: + - task_with_password is changed + - task_with_password_result.task_exists == True + - task_with_password_result.principal.group_id == None + - task_with_password_result.principal.logon_type == "TASK_LOGON_PASSWORD" + - task_with_password_result.principal.run_level == "TASK_RUNLEVEL_LUA" + - task_with_password_result.principal.user_id.endswith(test_scheduled_task_user) + +- name: task with password principal (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: '{{test_scheduled_task_user}}' + password: '{{test_scheduled_task_pass}}' + logon_type: password + update_password: no + actions: + - path: cmd.exe + register: task_with_password_again + +- name: assert results of task with password principal (idempotent) + assert: + that: + - task_with_password_again is not changed + +- name: task with password principal force pass change + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: '{{test_scheduled_task_user}}' + password: '{{test_scheduled_task_pass}}' + logon_type: password + update_password: yes + actions: + - path: cmd.exe + register: task_with_password_force_update + +- name: assert results of task with password principal force pass change + assert: + that: + - task_with_password_force_update is changed + +- name: task with s4u principal (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: '{{test_scheduled_task_user}}' + password: '{{test_scheduled_task_pass}}' + logon_type: s4u + update_password: no + actions: + - path: cmd.exe + register: task_with_s4u_check + check_mode: yes + +- name: get result of task with s4u principal (check mode) + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: task_with_s4u_result_check + +- name: assert results of task with s4u principal (check mode) + assert: + that: + - task_with_s4u_check is changed + - task_with_s4u_result_check.task_exists == True + - task_with_s4u_result_check.principal.group_id == None + - task_with_s4u_result_check.principal.logon_type == "TASK_LOGON_PASSWORD" + - task_with_s4u_result_check.principal.run_level == "TASK_RUNLEVEL_LUA" + - task_with_s4u_result_check.principal.user_id.endswith(test_scheduled_task_user) + +- name: task with s4u principal + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: '{{test_scheduled_task_user}}' + password: '{{test_scheduled_task_pass}}' + logon_type: s4u + update_password: no + actions: + - path: cmd.exe + register: task_with_s4u + +- name: get result of task with s4u principal + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: task_with_s4u_result + +- name: assert results of task with s4u principal + assert: + that: + - task_with_s4u is changed + - task_with_s4u_result.task_exists == True + - task_with_s4u_result.principal.group_id == None + - task_with_s4u_result.principal.logon_type == "TASK_LOGON_S4U" + - task_with_s4u_result.principal.run_level == "TASK_RUNLEVEL_LUA" + - task_with_s4u_result.principal.user_id.endswith(test_scheduled_task_user) + +- name: task with s4u principal (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: '{{test_scheduled_task_user}}' + password: '{{test_scheduled_task_pass}}' + logon_type: s4u + update_password: no + actions: + - path: cmd.exe + register: task_with_s4u_again + +- name: assert results of task with s4u principal (idempotent) + assert: + that: + - task_with_s4u_again is not changed + +- name: task with interactive principal (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: '{{test_scheduled_task_user}}' + logon_type: interactive_token + actions: + - path: cmd.exe + register: task_with_interactive_check + check_mode: yes + +- name: get result of task with interactive principal (check mode) + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: task_with_interactive_result_check + +- name: assert results of task with interactive principal (check mode) + assert: + that: + - task_with_interactive_check is changed + - task_with_interactive_result_check.task_exists == True + - task_with_interactive_result_check.principal.group_id == None + - task_with_interactive_result_check.principal.logon_type == "TASK_LOGON_S4U" + - task_with_interactive_result_check.principal.run_level == "TASK_RUNLEVEL_LUA" + - task_with_interactive_result_check.principal.user_id.endswith(test_scheduled_task_user) + +- name: task with interactive principal + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: '{{test_scheduled_task_user}}' + logon_type: interactive_token + actions: + - path: cmd.exe + register: task_with_interactive + +- name: get result of task with interactive principal + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: task_with_interactive_result + +- name: assert results of task with interactive principal + assert: + that: + - task_with_interactive is changed + - task_with_interactive_result.task_exists == True + - task_with_interactive_result.principal.group_id == None + - task_with_interactive_result.principal.logon_type == "TASK_LOGON_INTERACTIVE_TOKEN" + - task_with_interactive_result.principal.run_level == "TASK_RUNLEVEL_LUA" + - task_with_interactive_result.principal.user_id.endswith(test_scheduled_task_user) + +- name: task with interactive principal (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: '{{test_scheduled_task_user}}' + logon_type: interactive_token + actions: + - path: cmd.exe + register: task_with_interactive_again + +- name: assert results of task with interactive principal (idempotent) + assert: + that: + - task_with_interactive_again is not changed + +- name: task with group principal (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + group: Administrators + logon_type: group + actions: + - path: cmd.exe + register: task_with_group_check + check_mode: yes + +- name: get result of task with group principal (check mode) + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: task_with_group_result_check + +- name: assert results of task with group principal (check mode) + assert: + that: + - task_with_group_check is changed + - task_with_group_result_check.task_exists == True + - task_with_group_result_check.principal.group_id == None + - task_with_group_result_check.principal.logon_type == "TASK_LOGON_INTERACTIVE_TOKEN" + - task_with_group_result_check.principal.run_level == "TASK_RUNLEVEL_LUA" + - task_with_group_result_check.principal.user_id.endswith(test_scheduled_task_user) + +- name: task with group principal + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + group: Administrators + logon_type: group + actions: + - path: cmd.exe + register: task_with_group + +- name: get result of task with group principal + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: task_with_group_result + +- name: assert results of task with group principal + assert: + that: + - task_with_group is changed + - task_with_group_result.task_exists == True + - task_with_group_result.principal.group_id == "BUILTIN\\Administrators" + - task_with_group_result.principal.logon_type == "TASK_LOGON_GROUP" + - task_with_group_result.principal.run_level == "TASK_RUNLEVEL_LUA" + - task_with_group_result.principal.user_id == None + +- name: task with group principal (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + group: Administrators + logon_type: group + actions: + - path: cmd.exe + register: task_with_group_again + +- name: assert results of task with group principal (idempotent) + assert: + that: + - task_with_group_again is not changed + +- name: task with service account principal (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: System + logon_type: service_account + action: + - path: cmd.exe + register: task_with_service_check + check_mode: yes + +- name: get result of task with service account principal (check mode) + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: task_with_service_result_check + +- name: assert results of task with service account principal (check mode) + assert: + that: + - task_with_service_check is changed + - task_with_service_result_check.task_exists == True + - task_with_service_result_check.principal.group_id == "BUILTIN\\Administrators" + - task_with_service_result_check.principal.logon_type == "TASK_LOGON_GROUP" + - task_with_service_result_check.principal.run_level == "TASK_RUNLEVEL_LUA" + - task_with_service_result_check.principal.user_id == None + +- name: task with service account principal + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: System + logon_type: service_account + action: + - path: cmd.exe + register: task_with_service + +- name: get result of task with service account principal + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: task_with_service_result + +- name: assert results of task with service account principal + assert: + that: + - task_with_service is changed + - task_with_service_result.task_exists == True + - task_with_service_result.principal.group_id == None + - task_with_service_result.principal.logon_type == "TASK_LOGON_SERVICE_ACCOUNT" + - task_with_service_result.principal.run_level == "TASK_RUNLEVEL_LUA" + - task_with_service_result.principal.user_id == "NT AUTHORITY\\SYSTEM" + +- name: task with service account principal (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + username: System + logon_type: service_account + action: + - path: cmd.exe + register: task_with_service_again + +- name: assert results of task with service account principal (idempotent) + assert: + that: + - task_with_service_again is not changed + +- name: task with highest privilege (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + run_level: highest + username: System + logon_type: service_account + action: + - path: cmd.exe + register: task_with_highest_privilege_check + check_mode: yes + +- name: get result of task with highest privilege (check mode) + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: task_with_highest_privilege_result_check + +- name: assert results of task with highest privilege (check mode) + assert: + that: + - task_with_highest_privilege_check is changed + - task_with_highest_privilege_result_check.principal.run_level == "TASK_RUNLEVEL_LUA" + +- name: task with highest privilege + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + run_level: highest + username: System + logon_type: service_account + action: + - path: cmd.exe + register: task_with_highest_privilege + +- name: get result of task with highest privilege + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: task_with_highest_privilege_result + +- name: assert results of task with highest privilege + assert: + that: + - task_with_highest_privilege is changed + - task_with_highest_privilege_result.principal.run_level == "TASK_RUNLEVEL_HIGHEST" + +- name: task with highest privilege (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + run_level: highest + username: System + logon_type: service_account + action: + - path: cmd.exe + register: task_with_highest_privilege_again + +- name: assert results of task with highest privilege (idempotent) + assert: + that: + - task_with_highest_privilege_again is not changed diff --git a/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task/tasks/tests.yml new file mode 100644 index 000000000..b4ed29e49 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task/tasks/tests.yml @@ -0,0 +1,440 @@ +--- +- name: create task (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + arguments: /c echo hi + description: Original Description + register: create_task_check + check_mode: yes + +- name: get result of create task (check mode) + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: create_task_result_check + +- name: assert results of create task (check mode) + assert: + that: + - create_task_check is changed + - create_task_result_check.task_exists == False + +- name: create task + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + arguments: /c echo hi + description: Original Description + register: create_task + +- name: get result of create task + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: create_task_result + +- name: assert results of create task + assert: + that: + - create_task is changed + - create_task_result.task_exists == True + - create_task_result.actions|count == 1 + - create_task_result.actions[0].path == "cmd.exe" + - create_task_result.actions[0].arguments == "/c echo hi" + - create_task_result.actions[0].working_directory == None + - create_task_result.registration_info.description == "Original Description" + - create_task_result.triggers|count == 0 + +- name: create task (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + arguments: /c echo hi + description: Original Description + register: create_task_again + +- name: assert results of create task (idempotent) + assert: + that: + - create_task_again is not changed + +- name: change task (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + author: Cow Inc. + description: Test for Ansible + allow_demand_start: no + restart_count: 5 + restart_interval: PT2H5M + register: change_task_check + check_mode: yes + +- name: get result of change task (check mode) + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: change_task_result_check + +- name: assert results of change task (check mode) + assert: + that: + - change_task_check is changed + - change_task_result_check.actions|count == 1 + - change_task_result_check.registration_info.author == None + - change_task_result_check.registration_info.description == "Original Description" + - change_task_result_check.settings.allow_demand_start == true + - change_task_result_check.settings.restart_count == 0 + - change_task_result_check.settings.restart_interval == None + +- name: change task + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + author: Cow Inc. + description: Test for Ansible + allow_demand_start: no + restart_count: 5 + restart_interval: PT1M + register: change_task + +- name: get result of change task + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: change_task_result + +- name: assert results of change task + assert: + that: + - change_task is changed + - change_task_result.actions|count == 1 + - change_task_result.registration_info.author == "Cow Inc." + - change_task_result.registration_info.description == "Test for Ansible" + - change_task_result.settings.allow_demand_start == false + - change_task_result.settings.restart_count == 5 + - change_task_result.settings.restart_interval == "PT1M" + +- name: change task (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + author: Cow Inc. + description: Test for Ansible + allow_demand_start: no + restart_count: 5 + restart_interval: PT1M + register: change_task_again + +- name: assert results of change task (idempotent) + assert: + that: + - change_task_again is not changed + +- name: add task action (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + arguments: /c echo hi + - path: powershell.exe + arguments: -File C:\ansible\script.ps1 + working_directory: C:\ansible + register: add_task_action_check + check_mode: yes + +- name: get result of add task action (check mode) + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: add_task_action_result_check + +- name: assert results of add task action (check mode) + assert: + that: + - add_task_action_check is changed + - add_task_action_result_check.actions|count == 1 + - add_task_action_result_check.actions[0].path == "cmd.exe" + - add_task_action_result_check.actions[0].arguments == "/c echo hi" + - add_task_action_result_check.actions[0].working_directory == None + +- name: add task action + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + arguments: /c echo hi + - path: powershell.exe + arguments: -File C:\ansible\script.ps1 + working_directory: C:\ansible + register: add_task_action + +- name: get result of add task action + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: add_task_action_result + +- name: assert results of add task action + assert: + that: + - add_task_action is changed + - add_task_action_result.actions|count == 2 + - add_task_action_result.actions[0].path == "cmd.exe" + - add_task_action_result.actions[0].arguments == "/c echo hi" + - add_task_action_result.actions[0].working_directory == None + - add_task_action_result.actions[1].path == "powershell.exe" + - add_task_action_result.actions[1].arguments == "-File C:\\ansible\\script.ps1" + - add_task_action_result.actions[1].working_directory == "C:\\ansible" + +- name: add task action (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + arguments: /c echo hi + - path: powershell.exe + arguments: -File C:\ansible\script.ps1 + working_directory: C:\ansible + register: add_task_action_again + +- name: assert results of add task action (idempotent) + assert: + that: + - add_task_action_again is not changed + +- name: remove task action (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: powershell.exe + arguments: -File C:\ansible\script.ps1 + working_directory: C:\ansible + register: remove_task_action_check + check_mode: yes + +- name: get result of remove task action (check mode) + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: remove_task_action_result_check + +- name: assert results of remove task action (check mode) + assert: + that: + - remove_task_action_check is changed + - remove_task_action_result_check.actions|count == 2 + - remove_task_action_result_check.actions[0].path == "cmd.exe" + - remove_task_action_result_check.actions[0].arguments == "/c echo hi" + - remove_task_action_result_check.actions[0].working_directory == None + - remove_task_action_result_check.actions[1].path == "powershell.exe" + - remove_task_action_result_check.actions[1].arguments == "-File C:\\ansible\\script.ps1" + - remove_task_action_result_check.actions[1].working_directory == "C:\\ansible" + +- name: remove task action + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: powershell.exe + arguments: -File C:\ansible\script.ps1 + working_directory: C:\ansible + register: remove_task_action + +- name: get result of remove task action + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: remove_task_action_result + +- name: assert results of remove task action + assert: + that: + - remove_task_action is changed + - remove_task_action_result.actions|count == 1 + - remove_task_action_result.actions[0].path == "powershell.exe" + - remove_task_action_result.actions[0].arguments == "-File C:\\ansible\\script.ps1" + - remove_task_action_result.actions[0].working_directory == "C:\\ansible" + +- name: remove task action (idempontent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: powershell.exe + arguments: -File C:\ansible\script.ps1 + working_directory: C:\ansible + register: remove_task_action_again + +- name: assert results of remove task action (idempotent) + assert: + that: + - remove_task_action_again is not changed + +- name: remove task (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: absent + register: remove_task_check + check_mode: yes + +- name: get result of remove task (check mode) + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: remove_task_result_check + +- name: assert results of remove task (check mode) + assert: + that: + - remove_task_check is changed + - remove_task_result_check.task_exists == True + +- name: remove task + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: absent + register: remove_task + +- name: get result of remove task + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: remove_task_result + +- name: assert results of remove task + assert: + that: + - remove_task is changed + - remove_task_result.task_exists == False + +- name: remove task (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: absent + register: remove_task_again + +- name: assert results of remove task (idempotent) + assert: + that: + - remove_task_again is not changed + +- name: create sole task in folder (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + path: '{{test_scheduled_task_path}}' + actions: + - path: cmd.exe + register: create_sole_task_check + check_mode: yes + +- name: get result of create sole task in folder (check mode) + win_scheduled_task_stat: + path: '{{test_scheduled_task_path}}' + name: '{{test_scheduled_task_name}}' + register: create_sole_task_result_check + +- name: assert results of create sole task in folder (check mode) + assert: + that: + - create_sole_task_check is changed + - create_sole_task_result_check.folder_exists == False + - create_sole_task_result_check.task_exists == False + +- name: create sole task in folder + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + path: '{{test_scheduled_task_path}}' + actions: + - path: cmd.exe + register: create_sole_task + +- name: get result of create sole task in folder + win_scheduled_task_stat: + path: '{{test_scheduled_task_path}}' + name: '{{test_scheduled_task_name}}' + register: create_sole_task_result + +- name: assert results of create sole task in folder + assert: + that: + - create_sole_task is changed + - create_sole_task_result.folder_exists == True + - create_sole_task_result.task_exists == True + +- name: create sole task in folder (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + path: '{{test_scheduled_task_path}}' + actions: + - path: cmd.exe + register: create_sole_task_again + +- name: assert results of create sole task in folder (idempotent) + assert: + that: + - create_sole_task_again is not changed + +- name: remove sole task in folder (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + path: '{{test_scheduled_task_path}}' + state: absent + register: remove_sole_task_check + check_mode: yes + +- name: get result of remove sole task in folder (check mode) + win_scheduled_task_stat: + path: '{{test_scheduled_task_path}}' + name: '{{test_scheduled_task_name}}' + register: remove_sole_task_result_check + +- name: assert results of remove sole task in folder (check mode) + assert: + that: + - remove_sole_task_check is changed + - remove_sole_task_result_check.folder_exists == True + - remove_sole_task_result_check.task_exists == True + +- name: remove sole task in folder + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + path: '{{test_scheduled_task_path}}' + state: absent + register: remove_sole_task + +- name: get result of remove sole task in folder + win_scheduled_task_stat: + path: '{{test_scheduled_task_path}}' + name: '{{test_scheduled_task_name}}' + register: remove_sole_task_result + +- name: assert results of remove sole task in folder + assert: + that: + - remove_sole_task is changed + - remove_sole_task_result.folder_exists == False + - remove_sole_task_result.task_exists == False + +- name: remove sole task in folder (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + path: '{{test_scheduled_task_path}}' + state: absent + register: remove_sole_task_again + +- name: assert results of remove sole task in folder (idempotent) + assert: + that: + - remove_sole_task_again is not changed diff --git a/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task/tasks/triggers.yml b/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task/tasks/triggers.yml new file mode 100644 index 000000000..eae42c98a --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task/tasks/triggers.yml @@ -0,0 +1,851 @@ +--- +- name: create boot trigger (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: boot + register: trigger_boot_check + check_mode: yes + +- name: get result of create boot trigger (check mode) + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: trigger_boot_result_check + +- name: assert results of create boot trigger (check mode) + assert: + that: + - trigger_boot_check is changed + - trigger_boot_result_check.task_exists == False + +- name: create boot trigger + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: boot + register: trigger_boot + +- name: get result of create boot trigger + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: trigger_boot_result + +- name: assert results of create boot trigger + assert: + that: + - trigger_boot is changed + - trigger_boot_result.task_exists == True + - trigger_boot_result.triggers|count == 1 + - trigger_boot_result.triggers[0].type == "TASK_TRIGGER_BOOT" + - trigger_boot_result.triggers[0].enabled == True + - trigger_boot_result.triggers[0].start_boundary == None + - trigger_boot_result.triggers[0].end_boundary == None + +- name: create boot trigger (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: boot + register: trigger_boot_again + +- name: assert results of create boot trigger (idempotent) + assert: + that: + - trigger_boot_again is not changed + +- name: create daily trigger (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: daily + start_boundary: '2000-01-01T00:00:01' + register: trigger_daily_check + check_mode: yes + +- name: get result of create daily trigger (check mode) + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: trigger_daily_result_check + +- name: assert results of create daily trigger (check mode) + assert: + that: + - trigger_daily_check is changed + - trigger_daily_result_check.task_exists == True + - trigger_daily_result_check.triggers|count == 1 + - trigger_daily_result_check.triggers[0].type == "TASK_TRIGGER_BOOT" + - trigger_daily_result_check.triggers[0].enabled == True + - trigger_daily_result_check.triggers[0].start_boundary == None + - trigger_daily_result_check.triggers[0].end_boundary == None + +- name: create daily trigger + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: daily + start_boundary: '2000-01-01T00:00:01' + register: trigger_daily + +- name: get result of create daily trigger + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: trigger_daily_result + +- name: assert results of create daily trigger + assert: + that: + - trigger_daily is changed + - trigger_daily_result.task_exists == True + - trigger_daily_result.triggers|count == 1 + - trigger_daily_result.triggers[0].type == "TASK_TRIGGER_DAILY" + - trigger_daily_result.triggers[0].enabled == True + - trigger_daily_result.triggers[0].start_boundary == "2000-01-01T00:00:01" + - trigger_daily_result.triggers[0].end_boundary == None + +- name: create daily trigger (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: daily + start_boundary: '2000-01-01T00:00:01' + register: trigger_daily_again + +- name: assert results of create daily trigger (idempotent) + assert: + that: + - trigger_daily_again is not changed + +- name: create logon trigger (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: logon + register: trigger_logon_check + check_mode: yes + +- name: get result of create logon trigger (check mode) + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: trigger_logon_result_check + +- name: assert results of create logon trigger + assert: + that: + - trigger_logon_check is changed + - trigger_logon_result_check.task_exists == True + - trigger_logon_result_check.triggers|count == 1 + - trigger_logon_result_check.triggers[0].type == "TASK_TRIGGER_DAILY" + - trigger_logon_result_check.triggers[0].enabled == True + - trigger_logon_result_check.triggers[0].start_boundary == "2000-01-01T00:00:01" + - trigger_logon_result_check.triggers[0].end_boundary == None + +- name: create logon trigger + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: logon + register: trigger_logon + +- name: get result of create logon trigger + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: trigger_logon_result + +- name: assert results of create logon trigger + assert: + that: + - trigger_logon is changed + - trigger_logon_result.task_exists == True + - trigger_logon_result.triggers|count == 1 + - trigger_logon_result.triggers[0].type == "TASK_TRIGGER_LOGON" + - trigger_logon_result.triggers[0].enabled == True + - trigger_logon_result.triggers[0].start_boundary == None + - trigger_logon_result.triggers[0].end_boundary == None + +- name: create logon trigger (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: logon + register: trigger_logon_again + +- name: assert results of create logon trigger (idempotent) + assert: + that: + - trigger_logon_again is not changed + +- name: create monthly dow trigger (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: monthlydow + start_boundary: '2000-01-01T00:00:01' + weeks_of_month: 1,2 + run_on_last_week_of_month: true + days_of_week: [ "monday", "wednesday" ] + register: trigger_monthlydow_check + check_mode: yes + +- name: get result of create monthly dow trigger (check mode) + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: trigger_monthlydow_result_check + +- name: assert results of create monthly dow trigger (check mode) + assert: + that: + - trigger_monthlydow_check is changed + - trigger_monthlydow_result_check.task_exists == True + - trigger_monthlydow_result_check.triggers|count == 1 + - trigger_monthlydow_result_check.triggers[0].type == "TASK_TRIGGER_LOGON" + - trigger_monthlydow_result_check.triggers[0].enabled == True + - trigger_monthlydow_result_check.triggers[0].start_boundary == None + - trigger_monthlydow_result_check.triggers[0].end_boundary == None + +- name: create monthly dow trigger + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: monthlydow + start_boundary: '2000-01-01T00:00:01+03:00' + weeks_of_month: 1,2 + run_on_last_week_of_month: true + days_of_week: [ "monday", "wednesday" ] + register: trigger_monthlydow + +- name: get result of create monthly dow trigger + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: trigger_monthlydow_result + +- name: get expected date based on host timezone + ansible.windows.win_shell: (Get-Date '1999-12-31T21:00:01+00:00').ToString('yyyy-MM-ddTHH:mm:sszzz') + register: trigger_monthlydow_date + +- name: assert results of create monthly dow trigger + assert: + that: + - trigger_monthlydow is changed + - trigger_monthlydow_result.task_exists == True + - trigger_monthlydow_result.triggers|count == 1 + - trigger_monthlydow_result.triggers[0].type == "TASK_TRIGGER_MONTHLYDOW" + - trigger_monthlydow_result.triggers[0].enabled == True + - trigger_monthlydow_result.triggers[0].start_boundary == trigger_monthlydow_date.stdout|trim + - trigger_monthlydow_result.triggers[0].end_boundary == None + - trigger_monthlydow_result.triggers[0].weeks_of_month == "1,2" + - trigger_monthlydow_result.triggers[0].run_on_last_week_of_month == True + - trigger_monthlydow_result.triggers[0].days_of_week == "monday,wednesday" + +- name: create monthly dow trigger (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: monthlydow + start_boundary: '2000-01-01T00:00:01+03:00' + weeks_of_month: 1,2 + run_on_last_week_of_month: true + days_of_week: [ "monday", "wednesday" ] + register: trigger_monthlydow_again + +- name: assert results of create monthly dow trigger (idempotent) + assert: + that: + - trigger_monthlydow_again is not changed + +- name: create trigger repetition (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: registration + repetition: + # TODO: change to dict in 2.12 as a list format is deprecated + - interval: PT1M + duration: PT5M + stop_at_duration_end: yes + register: create_trigger_repetition_check + check_mode: yes + +- name: get result of create trigger repetition (check mode) + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: create_trigger_repetition_result_check + +- name: assert results of create trigger repetition (check mode) + assert: + that: + - create_trigger_repetition_check is changed + - create_trigger_repetition_check.deprecations|count == 1 + - create_trigger_repetition_check.deprecations[0].date == "2021-07-01" + - create_trigger_repetition_check.deprecations[0].msg == "repetition is a list, should be defined as a dict" + - create_trigger_repetition_result_check.task_exists == True + - create_trigger_repetition_result_check.triggers|count == 1 + - create_trigger_repetition_result_check.triggers[0].type == "TASK_TRIGGER_MONTHLYDOW" + - create_trigger_repetition_result_check.triggers[0].enabled == True + - create_trigger_repetition_result_check.triggers[0].start_boundary == trigger_monthlydow_date.stdout|trim + - create_trigger_repetition_result_check.triggers[0].end_boundary == None + - create_trigger_repetition_result_check.triggers[0].weeks_of_month == "1,2" + - create_trigger_repetition_result_check.triggers[0].run_on_last_week_of_month == True + - create_trigger_repetition_result_check.triggers[0].days_of_week == "monday,wednesday" + - create_trigger_repetition_result_check.triggers[0].repetition.interval == None + - create_trigger_repetition_result_check.triggers[0].repetition.duration == None + - create_trigger_repetition_result_check.triggers[0].repetition.stop_at_duration_end == False + +- name: create trigger repetition + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: registration + repetition: + interval: PT1M + duration: PT5M + stop_at_duration_end: yes + register: create_trigger_repetition + +- name: get result of create trigger repetition + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: create_trigger_repetition_result + +- name: assert results of create trigger repetition + assert: + that: + - create_trigger_repetition is changed + - create_trigger_repetition_result.task_exists == True + - create_trigger_repetition_result.triggers|count == 1 + - create_trigger_repetition_result.triggers[0].type == "TASK_TRIGGER_REGISTRATION" + - create_trigger_repetition_result.triggers[0].enabled == True + - create_trigger_repetition_result.triggers[0].start_boundary == None + - create_trigger_repetition_result.triggers[0].end_boundary == None + - create_trigger_repetition_result.triggers[0].repetition.interval == "PT1M" + - create_trigger_repetition_result.triggers[0].repetition.duration == "PT5M" + - create_trigger_repetition_result.triggers[0].repetition.stop_at_duration_end == True + +- name: create trigger repetition (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: registration + repetition: + interval: PT1M + duration: PT5M + stop_at_duration_end: yes + register: create_trigger_repetition_again + +- name: assert results of create trigger repetition (idempotent) + assert: + that: + - create_trigger_repetition_again is not changed + +- name: change trigger repetition (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: registration + repetition: + interval: PT10M + duration: PT20M + stop_at_duration_end: no + register: change_trigger_repetition_check + check_mode: yes + +- name: get result of change trigger repetition (check mode) + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: change_trigger_repetition_result_check + +- name: assert results of change trigger repetition (check mode) + assert: + that: + - change_trigger_repetition_check is changed + - change_trigger_repetition_result_check.task_exists == True + - change_trigger_repetition_result_check.triggers|count == 1 + - change_trigger_repetition_result_check.triggers[0].type == "TASK_TRIGGER_REGISTRATION" + - change_trigger_repetition_result_check.triggers[0].enabled == True + - change_trigger_repetition_result_check.triggers[0].start_boundary == None + - change_trigger_repetition_result_check.triggers[0].end_boundary == None + - change_trigger_repetition_result_check.triggers[0].repetition.interval == "PT1M" + - change_trigger_repetition_result_check.triggers[0].repetition.duration == "PT5M" + - change_trigger_repetition_result_check.triggers[0].repetition.stop_at_duration_end == True + +- name: change trigger repetition + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: registration + repetition: + interval: PT10M + duration: PT20M + stop_at_duration_end: no + register: change_trigger_repetition + +- name: get result of change trigger repetition + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: change_trigger_repetition_result + +- name: assert results of change trigger repetition + assert: + that: + - change_trigger_repetition is changed + - change_trigger_repetition_result.task_exists == True + - change_trigger_repetition_result.triggers|count == 1 + - change_trigger_repetition_result.triggers[0].type == "TASK_TRIGGER_REGISTRATION" + - change_trigger_repetition_result.triggers[0].enabled == True + - change_trigger_repetition_result.triggers[0].start_boundary == None + - change_trigger_repetition_result.triggers[0].end_boundary == None + - change_trigger_repetition_result.triggers[0].repetition.interval == "PT10M" + - change_trigger_repetition_result.triggers[0].repetition.duration == "PT20M" + - change_trigger_repetition_result.triggers[0].repetition.stop_at_duration_end == False + +- name: change trigger repetition (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: registration + repetition: + interval: PT10M + duration: PT20M + stop_at_duration_end: no + register: change_trigger_repetition_again + +- name: assert results of change trigger repetition (idempotent) + assert: + that: + - change_trigger_repetition_again is not changed + +- name: create trigger with state_change + win_scheduled_task: + name: '{{ test_scheduled_task_name }}' + state: present + actions: + - path: cmd.exe + triggers: + - type: session_state_change + state_change: session_lock + register: trigger_state_change + +- name: get result of create trigger with state change + win_scheduled_task_stat: + path: \ + name: '{{ test_scheduled_task_name }}' + register: trigger_state_change_result + +- name: assert results of create trigger with state_change + assert: + that: + - trigger_state_change is changed + - trigger_state_change_result.triggers|count == 1 + - trigger_state_change_result.triggers[0].type == "TASK_TRIGGER_SESSION_STATE_CHANGE" + - trigger_state_change_result.triggers[0].state_change == 7 + - trigger_state_change_result.triggers[0].state_change_str == "TASK_SESSION_LOCK" + - trigger_state_change_result.triggers[0].user_id == None + +- name: create task with multiple triggers (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: monthly + days_of_month: 1,5,10,15,20,25,30 + run_on_last_day_of_month: true + start_boundary: '2000-01-01T00:00:01' + months_of_year: + - march + - may + - july + - type: time + start_boundary: '2000-01-01T00:00:01' + random_delay: PT10M5S + register: create_multiple_triggers_check + check_mode: yes + +- name: get result of create task with multiple triggers (check mode) + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: create_multiple_triggers_result_check + +- name: assert results of create task with multiple triggers (check mode) + assert: + that: + - create_multiple_triggers_check is changed + - create_multiple_triggers_result_check.task_exists == True + - create_multiple_triggers_result_check.triggers|count == 1 + - create_multiple_triggers_result_check.triggers[0].type == "TASK_TRIGGER_SESSION_STATE_CHANGE" + - create_multiple_triggers_result_check.triggers[0].state_change == 7 + - create_multiple_triggers_result_check.triggers[0].state_change_str == "TASK_SESSION_LOCK" + - create_multiple_triggers_result_check.triggers[0].user_id == None + +- name: create task with multiple triggers + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: monthly + days_of_month: 1,5,10,15,20,25,30 + run_on_last_day_of_month: true + start_boundary: '2000-01-01T00:00:01' + months_of_year: + - march + - may + - july + - type: time + start_boundary: '2000-01-01T00:00:01' + random_delay: PT10M5S + register: create_multiple_triggers + +- name: get result of create task with multiple triggers + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: create_multiple_triggers_result + +- name: assert results of create task with multiple triggers + assert: + that: + - create_multiple_triggers is changed + - create_multiple_triggers_result.task_exists == True + - create_multiple_triggers_result.triggers|count == 2 + - create_multiple_triggers_result.triggers[0].type == "TASK_TRIGGER_MONTHLY" + - create_multiple_triggers_result.triggers[0].enabled == True + - create_multiple_triggers_result.triggers[0].start_boundary == "2000-01-01T00:00:01" + - create_multiple_triggers_result.triggers[0].end_boundary == None + - create_multiple_triggers_result.triggers[0].days_of_month == "1,5,10,15,20,25,30" + - create_multiple_triggers_result.triggers[0].months_of_year == "march,may,july" + - create_multiple_triggers_result.triggers[0].run_on_last_day_of_month == True + - create_multiple_triggers_result.triggers[1].type == "TASK_TRIGGER_TIME" + - create_multiple_triggers_result.triggers[1].enabled == True + - create_multiple_triggers_result.triggers[1].start_boundary == "2000-01-01T00:00:01" + - create_multiple_triggers_result.triggers[1].end_boundary == None + - create_multiple_triggers_result.triggers[1].random_delay == "PT10M5S" + +- name: create task with multiple triggers (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: monthly + days_of_month: 1,5,10,15,20,25,30 + run_on_last_day_of_month: true + start_boundary: '2000-01-01T00:00:01' + months_of_year: + - march + - may + - july + - type: time + start_boundary: '2000-01-01T00:00:01' + random_delay: PT10M5S + register: create_multiple_triggers_again + +- name: assert results of create task with multiple triggers (idempotent) + assert: + that: + - create_multiple_triggers_again is not changed + +- name: change task with multiple triggers (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: weekly + days_of_week: tuesday,friday + start_boundary: '2000-01-01T00:00:01' + - type: registration + enabled: no + register: change_multiple_triggers_check + check_mode: yes + +- name: get result of change task with multiple triggers (check mode) + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: change_multiple_triggers_result_check + +- name: assert results of change task with multiple triggers (check mode) + assert: + that: + - change_multiple_triggers_check is changed + - change_multiple_triggers_result_check.task_exists == True + - change_multiple_triggers_result_check.triggers|count == 2 + - change_multiple_triggers_result_check.triggers[0].type == "TASK_TRIGGER_MONTHLY" + - change_multiple_triggers_result_check.triggers[0].enabled == True + - change_multiple_triggers_result_check.triggers[0].start_boundary == "2000-01-01T00:00:01" + - change_multiple_triggers_result_check.triggers[0].end_boundary == None + - change_multiple_triggers_result_check.triggers[0].days_of_month == "1,5,10,15,20,25,30" + - change_multiple_triggers_result_check.triggers[0].months_of_year == "march,may,july" + - change_multiple_triggers_result_check.triggers[0].run_on_last_day_of_month == True + - change_multiple_triggers_result_check.triggers[1].type == "TASK_TRIGGER_TIME" + - change_multiple_triggers_result_check.triggers[1].enabled == True + - change_multiple_triggers_result_check.triggers[1].start_boundary == "2000-01-01T00:00:01" + - change_multiple_triggers_result_check.triggers[1].end_boundary == None + - change_multiple_triggers_result_check.triggers[1].random_delay == "PT10M5S" + +- name: change task with multiple triggers + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: weekly + days_of_week: tuesday,friday + start_boundary: '2000-01-01T00:00:01' + - type: registration + enabled: no + register: change_multiple_triggers + +- name: get result of change task with multiple triggers + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: change_multiple_triggers_result + +- name: assert results of change task with multiple triggers + assert: + that: + - change_multiple_triggers is changed + - change_multiple_triggers_result.task_exists == True + - change_multiple_triggers_result.triggers|count == 2 + - change_multiple_triggers_result.triggers[0].type == "TASK_TRIGGER_WEEKLY" + - change_multiple_triggers_result.triggers[0].enabled == True + - change_multiple_triggers_result.triggers[0].start_boundary == "2000-01-01T00:00:01" + - change_multiple_triggers_result.triggers[0].end_boundary == None + - change_multiple_triggers_result.triggers[0].days_of_week == "tuesday,friday" + - change_multiple_triggers_result.triggers[1].type == "TASK_TRIGGER_REGISTRATION" + - change_multiple_triggers_result.triggers[1].enabled == False + - change_multiple_triggers_result.triggers[1].start_boundary == None + - change_multiple_triggers_result.triggers[1].end_boundary == None + +- name: change task with multiple triggers (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: weekly + days_of_week: tuesday,friday + start_boundary: '2000-01-01T00:00:01' + - type: registration + enabled: no + register: change_multiple_triggers_again + +- name: assert results of change task with multiple triggers (idempotent) + assert: + that: + - change_multiple_triggers_again is not changed + +- name: remove trigger from multiple triggers (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: registration + enabled: no + register: remove_single_trigger_check + check_mode: yes + +- name: get result of remove trigger from multiple triggers (check mode) + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: remove_single_trigger_result_check + +- name: assert results of remove trigger from multiple triggers (check mode) + assert: + that: + - remove_single_trigger_check is changed + - remove_single_trigger_result_check.task_exists == True + - remove_single_trigger_result_check.triggers|count == 2 + - remove_single_trigger_result_check.triggers[0].type == "TASK_TRIGGER_WEEKLY" + - remove_single_trigger_result_check.triggers[0].enabled == True + - remove_single_trigger_result_check.triggers[0].start_boundary == "2000-01-01T00:00:01" + - remove_single_trigger_result_check.triggers[0].end_boundary == None + - remove_single_trigger_result_check.triggers[0].days_of_week == "tuesday,friday" + - remove_single_trigger_result_check.triggers[1].type == "TASK_TRIGGER_REGISTRATION" + - remove_single_trigger_result_check.triggers[1].enabled == False + - remove_single_trigger_result_check.triggers[1].start_boundary == None + - remove_single_trigger_result_check.triggers[1].end_boundary == None + +- name: remove trigger from multiple triggers + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: registration + enabled: no + register: remove_single_trigger + +- name: get result of remove trigger from multiple triggers + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: remove_single_trigger_result + +- name: assert results of remove trigger from multiple triggers + assert: + that: + - remove_single_trigger is changed + - remove_single_trigger_result.task_exists == True + - remove_single_trigger_result.triggers|count == 1 + - remove_single_trigger_result.triggers[0].type == "TASK_TRIGGER_REGISTRATION" + - remove_single_trigger_result.triggers[0].enabled == False + - remove_single_trigger_result.triggers[0].start_boundary == None + - remove_single_trigger_result.triggers[0].end_boundary == None + +- name: remove trigger from multiple triggers (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: + - type: registration + enabled: no + register: remove_single_trigger_again + +- name: assert results of remove trigger from multiple triggers (idempotent) + assert: + that: + - remove_single_trigger_again is not changed + +- name: remove all triggers (check mode) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: [] + register: remove_triggers_check + check_mode: yes + +- name: get result of remove all triggers (check mode) + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: remove_triggers_result_check + +- name: assert results of remove all triggers (check mode) + assert: + that: + - remove_triggers_check is changed + - remove_triggers_result_check.task_exists == True + - remove_triggers_result_check.triggers|count == 1 + - remove_triggers_result_check.triggers[0].type == "TASK_TRIGGER_REGISTRATION" + - remove_triggers_result_check.triggers[0].enabled == False + - remove_triggers_result_check.triggers[0].start_boundary == None + - remove_triggers_result_check.triggers[0].end_boundary == None + +- name: remove all triggers + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: [] + register: remove_triggers + +- name: get result of remove all triggers + win_scheduled_task_stat: + path: \ + name: '{{test_scheduled_task_name}}' + register: remove_triggers_result + +- name: assert results of remove all triggers + assert: + that: + - remove_triggers is changed + - remove_triggers_result.task_exists == True + - remove_triggers_result.triggers|count == 0 + +- name: remove all triggers (idempotent) + win_scheduled_task: + name: '{{test_scheduled_task_name}}' + state: present + actions: + - path: cmd.exe + triggers: [] + register: remove_triggers_again + +- name: assert results of remove all triggers (idempotent) + assert: + that: + - remove_triggers_again is not changed diff --git a/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task_stat/aliases b/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task_stat/aliases new file mode 100644 index 000000000..423ce3910 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task_stat/aliases @@ -0,0 +1 @@ +shippable/windows/group2 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task_stat/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task_stat/defaults/main.yml new file mode 100644 index 000000000..55a2b87b3 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task_stat/defaults/main.yml @@ -0,0 +1,5 @@ +--- +test_scheduled_task_stat_name: Test Task +test_scheduled_task_stat_path: \test path +test_scheduled_task_stat_username: testtaskuser +test_scheduled_task_stat_password: Password123!
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task_stat/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task_stat/tasks/main.yml new file mode 100644 index 000000000..736899487 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task_stat/tasks/main.yml @@ -0,0 +1,47 @@ +--- +- name: ensure task is deleted before test + win_scheduled_task: + name: '{{test_scheduled_task_stat_name}}' + path: '{{test_scheduled_task_stat_path}}' + state: absent + +- name: create test user for service execution + ansible.windows.win_user: + name: '{{test_scheduled_task_stat_username}}' + password: '{{test_scheduled_task_stat_password}}' + state: present + groups_action: add + groups: + - Users + register: user_info + +# Run actual tests +- block: + - name: normalise test account name + ansible.windows.win_powershell: + parameters: + SID: '{{ user_info.sid }}' + script: | + [CmdletBinding()] + param ([String]$SID) + + $Ansible.Changed = $false + ([System.Security.Principal.SecurityIdentifier]$SID).Translate([System.Security.Principal.NTAccount]).Value + register: test_normalised_username + + - set_fact: + test_normalised_username: '{{ test_normalised_username.output[0] }}' + + - include_tasks: tests.yml + + always: + - name: remove test user + ansible.windows.win_user: + name: '{{test_scheduled_task_stat_username}}' + state: absent + + - name: delete task after test + win_scheduled_task: + name: '{{test_scheduled_task_stat_name}}' + path: '{{test_scheduled_task_stat_path}}' + state: absent diff --git a/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task_stat/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task_stat/tasks/tests.yml new file mode 100644 index 000000000..b0c8454d1 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_scheduled_task_stat/tasks/tests.yml @@ -0,0 +1,175 @@ +--- +# folder stat tests + +# check_mode operation test +- name: check_mode - get stat of a folder that is missing + win_scheduled_task_stat: + path: '{{test_scheduled_task_stat_path}}' + register: stat_folder_missing + check_mode: True + +- name: assert that check_mode works + assert: + that: stat_folder_missing is not skipped + +- name: get stat of a folder that is missing + win_scheduled_task_stat: + path: '{{test_scheduled_task_stat_path}}' + register: stat_folder_missing + +- name: assert get stat of a folder that is missing + assert: + that: + - stat_folder_missing.folder_exists == False + +- name: get stat of existing folder + win_scheduled_task_stat: + path: \ + register: stat_folder_present + +- name: assert get stat of existing folder + assert: + that: + - stat_folder_present.folder_exists == True + - stat_folder_present.folder_task_count is defined + - stat_folder_present.folder_task_names is defined + +- name: create scheduled task in folder + win_scheduled_task: + path: '{{test_scheduled_task_stat_path}}' + name: '{{test_scheduled_task_stat_name}}' + state: present + logon_type: interactive_token + username: '{{ test_normalised_username }}' + author: Ansible Author + description: Fake description + execution_time_limit: PT23H + disallow_start_if_on_batteries: false + restart_count: 3 + restart_interval: PT15M + actions: + - path: cmd.exe + - path: C:\temp\some.exe + arguments: --help + working_directory: C:\temp + triggers: + - type: boot + delay: PT15M + - type: monthly + days_of_month: 5,15,30 + months_of_year: june,december + run_on_last_day_of_month: true + start_boundary: '2017-09-20T03:44:38' + - type: session_state_change + state_change: console_disconnect + user_id: '{{ test_normalised_username }}' + +- name: get stat of existing folder with task + win_scheduled_task_stat: + path: '{{test_scheduled_task_stat_path}}' + register: stat_folder_with_task + +- name: assert get stat of existing folder with task + assert: + that: + - stat_folder_with_task.folder_exists == True + - stat_folder_with_task.folder_task_count == 1 + - stat_folder_with_task.folder_task_names[0] == "Test Task" + - stat_folder_with_task.task_exists is not defined + +# task stat tests +- name: get stat of missing task with invalid folder + win_scheduled_task_stat: + path: fake path + name: fake task + register: stat_task_missing_folder + +- name: assert get stat of missing task with invalid folder + assert: + that: + - stat_task_missing_folder.folder_exists == False + - stat_task_missing_folder.task_exists == False + +- name: get stat of missing task + win_scheduled_task_stat: + path: '{{test_scheduled_task_stat_path}}' + name: fake task + register: stat_task_missing + +- name: assert get stat of missing task + assert: + that: + - stat_task_missing.task_exists == False + +- name: get stat of existing task + win_scheduled_task_stat: + path: '{{test_scheduled_task_stat_path}}' + name: '{{test_scheduled_task_stat_name}}' + register: stat_task_present + +- name: assert get stat of existing task + assert: + that: + - stat_task_present.task_exists == True + - stat_task_present.actions|count == 2 + - stat_task_present.actions[0].path == "cmd.exe" + - stat_task_present.actions[0].type == "TASK_ACTION_EXEC" + - stat_task_present.actions[0].working_directory == None + - stat_task_present.actions[1].arguments == "--help" + - stat_task_present.actions[1].path == "C:\\temp\some.exe" + - stat_task_present.actions[1].type == "TASK_ACTION_EXEC" + - stat_task_present.actions[1].working_directory == "C:\\temp" + - stat_task_present.principal.display_name == None + - stat_task_present.principal.group_id == None + - stat_task_present.principal.logon_type == "TASK_LOGON_INTERACTIVE_TOKEN" + - stat_task_present.principal.run_level == "TASK_RUNLEVEL_LUA" + - stat_task_present.principal.user_id == test_normalised_username + - stat_task_present.registration_info.author == "Ansible Author" + - stat_task_present.registration_info.date is defined + - stat_task_present.registration_info.description == "Fake description" + - stat_task_present.settings.disallow_start_if_on_batteries == False + - stat_task_present.settings.execution_time_limit == "PT23H" + - stat_task_present.settings.restart_count == 3 + - stat_task_present.settings.restart_interval == "PT15M" + - stat_task_present.state.status == "TASK_STATE_READY" + - stat_task_present.triggers|count == 3 + - stat_task_present.triggers[0].delay == "PT15M" + - stat_task_present.triggers[0].type == "TASK_TRIGGER_BOOT" + - stat_task_present.triggers[0].repetition.stop_at_duration_end == False + - stat_task_present.triggers[0].repetition.duration == None + - stat_task_present.triggers[0].repetition.interval == None + - stat_task_present.triggers[1].days_of_month == "5,15,30" + - stat_task_present.triggers[1].months_of_year == "june,december" + - stat_task_present.triggers[1].run_on_last_day_of_month == True + - stat_task_present.triggers[1].start_boundary == "2017-09-20T03:44:38" + - stat_task_present.triggers[1].type == "TASK_TRIGGER_MONTHLY" + - stat_task_present.triggers[1].repetition.stop_at_duration_end == False + - stat_task_present.triggers[1].repetition.duration == None + - stat_task_present.triggers[1].repetition.interval == None + - stat_task_present.triggers[2].type == "TASK_TRIGGER_SESSION_STATE_CHANGE" + - stat_task_present.triggers[2].user_id == test_normalised_username + - stat_task_present.triggers[2].state_change == 2 + - stat_task_present.triggers[2].state_change_str == "TASK_CONSOLE_DISCONNECT" + +- name: change principal to system account so it will run in the next step + win_scheduled_task: + name: '{{test_scheduled_task_stat_name}}' + path: '{{test_scheduled_task_stat_path}}' + username: SYSTEM + +- name: start the scheduled task + ansible.windows.win_command: schtasks.exe /Run /TN "{{test_scheduled_task_stat_path}}\{{test_scheduled_task_stat_name}}" + +- name: get stat of running task + win_scheduled_task_stat: + path: '{{test_scheduled_task_stat_path}}' + name: '{{test_scheduled_task_stat_name}}' + register: stat_task_running + +- name: assert stat of running task + assert: + that: + - stat_task_running.state.status == "TASK_STATE_RUNNING" + +- name: stop the scheduled task + ansible.windows.win_command: schtasks.exe /End /TN "{{test_scheduled_task_stat_path}}\{{test_scheduled_task_stat_name}}" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_scoop/aliases b/ansible_collections/community/windows/tests/integration/targets/win_scoop/aliases new file mode 100644 index 000000000..e2eacc2b6 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_scoop/aliases @@ -0,0 +1,3 @@ +shippable/windows/group3 +skip/windows/2012 # Need pwsh 5+ +skip/windows/2012-R2 # Need pwsh 5+ diff --git a/ansible_collections/community/windows/tests/integration/targets/win_scoop/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_scoop/defaults/main.yml new file mode 100644 index 000000000..10bf6204b --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_scoop/defaults/main.yml @@ -0,0 +1,6 @@ +--- +test_scoop_package1: grep +test_scoop_package2: sed +test_scoop_packages: + - '{{ test_scoop_package1 }}' + - '{{ test_scoop_package2 }}' diff --git a/ansible_collections/community/windows/tests/integration/targets/win_scoop/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_scoop/tasks/main.yml new file mode 100644 index 000000000..895cdffe2 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_scoop/tasks/main.yml @@ -0,0 +1,8 @@ +--- +- name: ensure test packages are uninstalled + win_scoop: + name: '{{ test_scoop_packages }}' + state: absent + +- name: run tests + include_tasks: tests.yml diff --git a/ansible_collections/community/windows/tests/integration/targets/win_scoop/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_scoop/tasks/tests.yml new file mode 100644 index 000000000..b773e61cb --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_scoop/tasks/tests.yml @@ -0,0 +1,159 @@ +--- +- name: install package check mode + win_scoop: + name: '{{ test_scoop_package1 }}' + check_mode: yes + register: install_check + +- name: assert install package check mode + assert: + that: + - install_check is changed + - install_check.stdout is not defined + +- name: install package locally + win_scoop: + name: '{{ test_scoop_package1 }}' + register: install_locally + +- name: assert install package locally + assert: + that: + - install_locally is changed + +- name: install package locally (idempotent) + win_scoop: + name: '{{ test_scoop_package1 }}' + register: install_idempotent + +- name: assert install package locally (idempotent) + assert: + that: + - not install_idempotent is changed + +- name: install package globally + win_scoop: + name: '{{ test_scoop_package1 }}' + global: yes + register: install_globally + +- name: assert install package globally + assert: + that: + - install_globally is changed + +- name: install package globally + win_scoop: + name: '{{ test_scoop_package1 }}' + global: yes + register: install_globally_idempotent + +- name: assert install package globally (idempotent) + assert: + that: + - not install_globally_idempotent is changed + +- name: remove package + win_scoop: + name: '{{ test_scoop_package1 }}' + state: absent + register: remove + +- name: assert remove package + assert: + that: + - remove is changed + +- name: remove package (idempotent) + win_scoop: + name: '{{ test_scoop_package1 }}' + state: absent + register: remove_idempotent + +- name: assert remove package (idempotent) + assert: + that: + - not remove_idempotent is changed + +- name: remove package globally + win_scoop: + name: '{{ test_scoop_package1 }}' + state: absent + global: yes + register: remove_globally + +- name: assert remove package + assert: + that: + - remove_globally is changed + +- name: remove package globally (idempotent) + win_scoop: + name: '{{ test_scoop_package1 }}' + state: absent + global: yes + register: remove_globally_idempotent + +- name: assert remove package globally (idempotent) + assert: + that: + - not remove_globally_idempotent is changed + +- name: install package before check mode test + win_scoop: + name: '{{ test_scoop_package1 }}' + +- name: remove package check mode + win_scoop: + name: '{{ test_scoop_package1 }}' + state: absent + check_mode: yes + register: remove_check + +- name: assert remove package check mode + assert: + that: + - remove_check is changed + - remove_check.stdout is not defined + +- name: install multiple packages locally + win_scoop: + name: '{{ test_scoop_packages }}' + register: install_multiple + +- name: assert install multiple packages locally + assert: + that: + - install_multiple is changed + +- name: install multiple packages locally (idempotent) + win_scoop: + name: '{{ test_scoop_packages }}' + register: install_multiple_idempotent + +- name: assert install multiple packages locally (idempotent) + assert: + that: + - not install_multiple_idempotent is changed + +- name: remove multiple packages locally + win_scoop: + name: '{{ test_scoop_packages }}' + state: absent + register: remove_multiple + +- name: assert remove multiple packages + assert: + that: + - remove_multiple is changed + +- name: remove multiple packages locally (idempotent) + win_scoop: + name: '{{ test_scoop_packages }}' + state: absent + register: remove_multiple_idempotent + +- name: remove multiple packages locally (idempotent) + assert: + that: + - not remove_multiple_idempotent is changed diff --git a/ansible_collections/community/windows/tests/integration/targets/win_scoop_bucket/aliases b/ansible_collections/community/windows/tests/integration/targets/win_scoop_bucket/aliases new file mode 100644 index 000000000..9a9d0737d --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_scoop_bucket/aliases @@ -0,0 +1,3 @@ +shippable/windows/group2 +skip/windows/2012 # Need pwsh 5+ +skip/windows/2012-R2 # Need pwsh 5+ diff --git a/ansible_collections/community/windows/tests/integration/targets/win_scoop_bucket/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_scoop_bucket/defaults/main.yml new file mode 100644 index 000000000..c39f462e1 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_scoop_bucket/defaults/main.yml @@ -0,0 +1,9 @@ +--- +test_scoop_bucket1: + name: extras +test_scoop_bucket2: + name: versions + repo: https://github.com/ScoopInstaller/Versions +test_scoop_buckets: + - '{{ test_scoop_bucket1 }}' + - '{{ test_scoop_bucket2 }}' diff --git a/ansible_collections/community/windows/tests/integration/targets/win_scoop_bucket/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_scoop_bucket/tasks/main.yml new file mode 100644 index 000000000..fd7d4dac2 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_scoop_bucket/tasks/main.yml @@ -0,0 +1,18 @@ +--- +- name: install git + win_scoop: + name: git + +- name: ensure test buckets are removed + win_scoop_bucket: + name: '{{ item.name }}' + state: absent + with_items: '{{ test_scoop_buckets }}' + +- name: run tests + include_tasks: tests.yml + +- name: uninstall git + win_scoop: + name: git + state: absent diff --git a/ansible_collections/community/windows/tests/integration/targets/win_scoop_bucket/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_scoop_bucket/tasks/tests.yml new file mode 100644 index 000000000..f98eb4677 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_scoop_bucket/tasks/tests.yml @@ -0,0 +1,86 @@ +--- +- name: add known bucket + win_scoop_bucket: + name: '{{ test_scoop_bucket1.name }}' + register: add_known + +- name: assert add add known bucket + assert: + that: + - add_known is changed + +- name: add known bucket (idempotent) + win_scoop_bucket: + name: '{{ test_scoop_bucket1.name }}' + register: add_known_idempotent + +- name: assert add known bucket (idempotent) + assert: + that: + - not add_known_idempotent is changed + +- name: remove known bucket + win_scoop_bucket: + name: '{{ test_scoop_bucket1.name }}' + state: absent + register: remove_known + +- name: assert remove known bucket + assert: + that: + - remove_known is changed + +- name: remove known bucket (idempotent) + win_scoop_bucket: + name: '{{ test_scoop_bucket1.name }}' + state: absent + register: remove_known_idempotent + +- name: assert remove package + assert: + that: + - not remove_known_idempotent is changed + +- name: add custom bucket + win_scoop_bucket: + name: '{{ test_scoop_bucket2.name }}' + repo: '{{ test_scoop_bucket2.repo }}' + register: add_custom + +- name: assert add add custom bucket + assert: + that: + - add_custom is changed + +- name: add custom bucket (idempotent) + win_scoop_bucket: + name: '{{ test_scoop_bucket2.name }}' + repo: '{{ test_scoop_bucket2.repo }}' + register: add_custom_idempotent + +- name: assert add custom bucket (idempotent) + assert: + that: + - not add_custom_idempotent is changed + +- name: remove custom bucket + win_scoop_bucket: + name: '{{ test_scoop_bucket2.name }}' + state: absent + register: remove_custom + +- name: assert remove custom bucket + assert: + that: + - remove_custom is changed + +- name: remove custom bucket (idempotent) + win_scoop_bucket: + name: '{{ test_scoop_bucket2.name }}' + state: absent + register: remove_custom_idempotent + +- name: assert remove package + assert: + that: + - not remove_custom_idempotent is changed
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_security_policy/aliases b/ansible_collections/community/windows/tests/integration/targets/win_security_policy/aliases new file mode 100644 index 000000000..4cd27b3cb --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_security_policy/aliases @@ -0,0 +1 @@ +shippable/windows/group1 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_security_policy/library/test_win_security_policy.ps1 b/ansible_collections/community/windows/tests/integration/targets/win_security_policy/library/test_win_security_policy.ps1 new file mode 100644 index 000000000..caafbdddb --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_security_policy/library/test_win_security_policy.ps1 @@ -0,0 +1,55 @@ +#!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/ansible_collections/community/windows/tests/integration/targets/win_security_policy/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_security_policy/tasks/main.yml new file mode 100644 index 000000000..4ea4a0522 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_security_policy/tasks/main.yml @@ -0,0 +1,71 @@ +--- +- 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 + +- name: get current value for the LegalNoticeText + ansible.windows.win_reg_stat: + path: HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System + name: LegalNoticeText + register: original_legal_notice + +- 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: set LegalNoticeText with non-ASCII chars + ansible.windows.win_regedit: + path: HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System + name: LegalNoticeText + data: "Légal Noticé\r\nNewline" + + - name: run tests + include_tasks: tests.yml + + # https://github.com/ansible-collections/community.windows/issues/153 + - name: get the value for the LegalNoticeText after running tests + ansible.windows.win_reg_stat: + path: HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System + name: LegalNoticeText + register: legal_notice + + - name: verify the non-ASCII chars weren't corrupted + assert: + that: + - legal_notice.value == "Légal Noticé\r\nNewline" + + 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}}" + + - name: reset LegalNoticeText + ansible.windows.win_regedit: + path: HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System + name: LegalNoticeText + data: '{{ original_legal_notice.value }}' diff --git a/ansible_collections/community/windows/tests/integration/targets/win_security_policy/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_security_policy/tasks/tests.yml new file mode 100644 index 000000000..724b6010a --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/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/ansible_collections/community/windows/tests/integration/targets/win_shortcut/aliases b/ansible_collections/community/windows/tests/integration/targets/win_shortcut/aliases new file mode 100644 index 000000000..4cd27b3cb --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_shortcut/aliases @@ -0,0 +1 @@ +shippable/windows/group1 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_shortcut/tasks/clean.yml b/ansible_collections/community/windows/tests/integration/targets/win_shortcut/tasks/clean.yml new file mode 100644 index 000000000..3a9c9051e --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_shortcut/tasks/clean.yml @@ -0,0 +1,37 @@ +# Test code for the file module. +# (c) 2017, Dag Wieers <dag@wieers.com> + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +- name: Clean up Ansible website link + ansible.windows.win_file: + path: '%UserProfile%\Desktop\Ansible website.url' + state: absent + +- name: Clean up Registry Editor shortcut + ansible.windows.win_file: + path: '%Public%\Desktop\Registry Editor.lnk' + state: absent + +- name: Clean up Shell path shortcut + ansible.windows.win_file: + path: '%Public%\bin.lnk' + state: absent + +- name: Clean up Executable shortcut + ansible.windows.win_file: + path: '%Public%\Desktop\cmd.lnk' + state: absent diff --git a/ansible_collections/community/windows/tests/integration/targets/win_shortcut/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_shortcut/tasks/main.yml new file mode 100644 index 000000000..cf04ea3b5 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_shortcut/tasks/main.yml @@ -0,0 +1,34 @@ +# Test code for the file module. +# (c) 2017, Dag Wieers <dag@wieers.com> + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +- name: Clean slate + import_tasks: clean.yml + +- name: Test in normal mode + import_tasks: tests.yml + vars: + in_check_mode: no + +- name: Clean slate + import_tasks: clean.yml + +- name: Test in check-mode + import_tasks: tests.yml + vars: + in_check_mode: yes + check_mode: yes diff --git a/ansible_collections/community/windows/tests/integration/targets/win_shortcut/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_shortcut/tasks/tests.yml new file mode 100644 index 000000000..79e145c06 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_shortcut/tasks/tests.yml @@ -0,0 +1,367 @@ +# Test code for the file module. +# (c) 2017, Dag Wieers <dag@wieers.com> + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +- name: get current user profile location + raw: $env:USERPROFILE + check_mode: no + register: profile_result + +- set_fact: + profile_dir: '{{ profile_result.stdout_lines[0] }}' + +- name: Add Ansible website link on the desktop + win_shortcut: + src: https://ansible.com/ + dest: '%UserProfile%\Desktop\Ansible website.url' + state: present + register: ansible_website_link_add + +- name: Check there was a change + assert: + that: + - ansible_website_link_add.changed == true + - ansible_website_link_add.dest == profile_dir + '\Desktop\Ansible website.url' + - ansible_website_link_add.src == 'https://ansible.com/' + +- name: Add Ansible website link on the desktop again + win_shortcut: + src: https://ansible.com/ + dest: '%UserProfile%\Desktop\Ansible website.url' + state: present + register: ansible_website_link_add_again + +- name: Check there was no change (normal mode) + assert: + that: + - ansible_website_link_add_again.changed == false + - ansible_website_link_add_again.dest == profile_dir + '\Desktop\Ansible website.url' + - ansible_website_link_add_again.src == 'https://ansible.com/' + when: not in_check_mode + +- name: Check there was a change (check-mode) + assert: + that: + - ansible_website_link_add_again.changed == true + - ansible_website_link_add_again.dest == profile_dir + '\Desktop\Ansible website.url' + - ansible_website_link_add_again.src == 'https://ansible.com/' + when: in_check_mode + +- name: Remove link + win_shortcut: + dest: '%UserProfile%\Desktop\Ansible website.url' + state: absent + register: ansible_website_link_remove + +- name: Check there was a change (normal mode) + assert: + that: + - ansible_website_link_remove.changed == true + - ansible_website_link_remove.dest == profile_dir + '\Desktop\Ansible website.url' + when: not in_check_mode + +- name: Check there was no change (check-mode) + assert: + that: + - ansible_website_link_remove.changed == false + - ansible_website_link_remove.dest == profile_dir + '\Desktop\Ansible website.url' + when: in_check_mode + +- name: Remove link again + win_shortcut: + dest: '%UserProfile%\Desktop\Ansible website.url' + state: absent + register: ansible_website_link_remove_again + +- name: Check there was no change + assert: + that: + - ansible_website_link_remove_again.changed == false + - ansible_website_link_remove_again.dest == profile_dir + '\Desktop\Ansible website.url' + +- name: Add a regedit shortcut on the desktop + win_shortcut: + description: "Registry Editor" + src: regedit.exe + dest: '%Public%\Desktop\Registry Editor.lnk' + state: present + register: regedit_shortcut_add + +- name: Check there was a change + assert: + that: + - regedit_shortcut_add.changed == true + - regedit_shortcut_add.description == 'Registry Editor' + - regedit_shortcut_add.dest == 'C:\\Users\\Public\\Desktop\\Registry Editor.lnk' + - regedit_shortcut_add.src == 'C:\\Windows\\regedit.exe' + +- name: Add a regedit shortcut on the desktop again + win_shortcut: + description: "Registry Editor" + src: regedit.exe + dest: '%Public%\Desktop\Registry Editor.lnk' + state: present + register: regedit_shortcut_add_again + +- name: Check there was no change (normal mode) + assert: + that: + - regedit_shortcut_add_again.changed == false + - regedit_shortcut_add_again.description == 'Registry Editor' + - regedit_shortcut_add_again.dest == 'C:\\Users\\Public\\Desktop\\Registry Editor.lnk' + - regedit_shortcut_add_again.src == 'C:\\Windows\\regedit.exe' + when: not in_check_mode + +- name: Check there was a change (check-mode) + assert: + that: + - regedit_shortcut_add_again.changed == true + - regedit_shortcut_add_again.description == 'Registry Editor' + - regedit_shortcut_add_again.dest == 'C:\\Users\\Public\\Desktop\\Registry Editor.lnk' + - regedit_shortcut_add_again.src == 'C:\\Windows\\regedit.exe' + when: in_check_mode + +- name: Update a regedit shortcut on the desktop + win_shortcut: + description: "Registry Editor" + src: C:\BogusPath\regedit.exe + dest: '%Public%\Desktop\Registry Editor.lnk' + state: present + register: regedit_shortcut_update + +- name: Check there was a change + assert: + that: + - regedit_shortcut_update.changed == true + - regedit_shortcut_update.description == 'Registry Editor' + - regedit_shortcut_update.dest == 'C:\\Users\\Public\\Desktop\\Registry Editor.lnk' + - regedit_shortcut_update.src == 'C:\\BogusPath\\regedit.exe' + +- name: Update a regedit shortcut on the desktop again + win_shortcut: + description: "Registry Editor" + src: C:\BogusPath\regedit.exe + dest: '%Public%\Desktop\Registry Editor.lnk' + state: present + register: regedit_shortcut_update_again + +- name: Check there was no change (normal mode) + assert: + that: + - regedit_shortcut_update_again.changed == false + - regedit_shortcut_update_again.description == 'Registry Editor' + - regedit_shortcut_update_again.dest == 'C:\\Users\\Public\\Desktop\\Registry Editor.lnk' + - regedit_shortcut_update_again.src == 'C:\\BogusPath\\regedit.exe' + when: not in_check_mode + +- name: Check there was a change (check-mode) + assert: + that: + - regedit_shortcut_update_again.changed == true + - regedit_shortcut_update_again.description == 'Registry Editor' + - regedit_shortcut_update_again.dest == 'C:\\Users\\Public\\Desktop\\Registry Editor.lnk' + - regedit_shortcut_update_again.src == 'C:\\BogusPath\\regedit.exe' + when: in_check_mode + +- name: Add an (explicit) icon + win_shortcut: + description: "Registry Editor" + src: C:\Windows\regedit.exe + dest: '%Public%\Desktop\Registry Editor.lnk' + icon: 'C:\Windows\regedit.exe,0' + state: present + register: regedit_shortcut_add_icon + +- name: Check there was a change + assert: + that: + - regedit_shortcut_add_icon.changed == true + - regedit_shortcut_add_icon.description == 'Registry Editor' + - regedit_shortcut_add_icon.dest == 'C:\\Users\\Public\\Desktop\\Registry Editor.lnk' + - regedit_shortcut_add_icon.icon == 'C:\\Windows\\regedit.exe,0' + - regedit_shortcut_add_icon.src == 'C:\\Windows\\regedit.exe' + +- name: Add an (explicit) icon again + win_shortcut: + description: "Registry Editor" + src: C:\Windows\regedit.exe + dest: '%Public%\Desktop\Registry Editor.lnk' + icon: 'C:\Windows\regedit.exe,0' + state: present + register: regedit_shortcut_add_icon_again + +- name: Check there was no change (normal mode) + assert: + that: + - regedit_shortcut_add_icon_again.changed == false + - regedit_shortcut_add_icon_again.description == 'Registry Editor' + - regedit_shortcut_add_icon_again.dest == 'C:\\Users\\Public\\Desktop\\Registry Editor.lnk' + - regedit_shortcut_add_icon_again.icon == 'C:\\Windows\\regedit.exe,0' + - regedit_shortcut_add_icon_again.src == 'C:\\Windows\\regedit.exe' + when: not in_check_mode + +- name: Check there was a change (check-mode) + assert: + that: + - regedit_shortcut_add_icon_again.changed == true + - regedit_shortcut_add_icon_again.description == 'Registry Editor' + - regedit_shortcut_add_icon_again.dest == 'C:\\Users\\Public\\Desktop\\Registry Editor.lnk' + - regedit_shortcut_add_icon_again.icon == 'C:\\Windows\\regedit.exe,0' + - regedit_shortcut_add_icon_again.src == 'C:\\Windows\\regedit.exe' + when: in_check_mode + +- name: Remove shortcut + win_shortcut: + dest: '%Public%\Desktop\Registry Editor.lnk' + state: absent + register: regedit_shortcut_remove + +- name: Check there was a change (normal mode) + assert: + that: + - regedit_shortcut_remove.changed == true + - regedit_shortcut_remove.dest == 'C:\\Users\\Public\\Desktop\\Registry Editor.lnk' + when: not in_check_mode + +- name: Check there was no change (check-mode) + assert: + that: + - regedit_shortcut_remove.changed == false + - regedit_shortcut_remove.dest == 'C:\\Users\\Public\\Desktop\\Registry Editor.lnk' + when: in_check_mode + +- name: Remove shortcut again + win_shortcut: + dest: '%Public%\Desktop\Registry Editor.lnk' + state: absent + register: regedit_shortcut_remove_again + +- name: Check there was no change + assert: + that: + - regedit_shortcut_remove_again.changed == false + - regedit_shortcut_remove_again.dest == 'C:\\Users\\Public\\Desktop\\Registry Editor.lnk' + +- name: Create shortcut to shell path + win_shortcut: + dest: '%Public%\bin.lnk' + src: shell:RecycleBinFolder + state: present + register: shell_add + +- name: Check there was a change + assert: + that: + - shell_add is changed + - shell_add.dest == 'C:\\Users\\Public\\bin.lnk' + - shell_add.src == 'shell:RecycleBinFolder' + +- name: Create shortcut to shell path again + win_shortcut: + dest: '%Public%\bin.lnk' + src: shell:RecycleBinFolder + state: present + register: shell_add_again + +- name: Check there was no change (normal mode) + assert: + that: + - not shell_add_again is changed + - shell_add_again.src == 'shell:RecycleBinFolder' + when: not in_check_mode + +- name: Check there was a change (check-mode) + assert: + that: + - shell_add_again is changed + when: in_check_mode + +- name: Change shortcut to another shell path + win_shortcut: + dest: '%Public%\bin.lnk' + src: shell:Start Menu + state: present + register: shell_change + +- name: Check there was a change + assert: + that: + - shell_change is changed + - shell_change.src == 'shell:Start Menu' + +- name: Create shortcut to an executable without run as admin + win_shortcut: + dest: '%Public%\Desktop\cmd.lnk' + src: '%SystemRoot%\System32\cmd.exe' + state: present + register: shell_exe_limited + +- name: Get run as admin flag state + ansible.windows.win_shell: | + $shortcut = "$env:Public\Desktop\cmd.lnk" + $flags = [System.BitConverter]::ToUInt32([System.IO.FIle]::ReadAllBytes($shortcut), 20) + ($flags -band 0x00002000) -eq 0x00002000 + register: shell_exe_limited_actual + +- name: Check that run as admin flag wasn't set (normal mode) + assert: + that: + - shell_exe_limited is changed + - not shell_exe_limited_actual.stdout_lines[0]|bool + when: not in_check_mode + +- name: Check that exe shortcut results in a change (check-mode) + assert: + that: + - shell_exe_limited is changed + when: in_check_mode + +- name: Set shortcut to run as admin + win_shortcut: + dest: '%Public%\Desktop\cmd.lnk' + src: '%SystemRoot%\System32\cmd.exe' + run_as_admin: True + state: present + register: shell_exe_admin + +- name: Get run as admin flag state + ansible.windows.win_shell: | + $shortcut = "$env:Public\Desktop\cmd.lnk" + $flags = [System.BitConverter]::ToUInt32([System.IO.FIle]::ReadAllBytes($shortcut), 20) + ($flags -band 0x00002000) -eq 0x00002000 + register: shell_exe_admin_actual + +- name: Check that run as admin flag was set (normal mode) + assert: + that: + - shell_exe_admin is changed + - shell_exe_admin_actual.stdout_lines[0]|bool + when: not in_check_mode + +- name: Set shortcut to run as admin again + win_shortcut: + dest: '%Public%\Desktop\cmd.lnk' + src: '%SystemRoot%\System32\cmd.exe' + run_as_admin: True + state: present + register: shell_exe_admin_again + +- name: Check that set run as admin wasn't changed (normal mode) + assert: + that: + - not shell_exe_admin_again is changed + when: not in_check_mode diff --git a/ansible_collections/community/windows/tests/integration/targets/win_snmp/aliases b/ansible_collections/community/windows/tests/integration/targets/win_snmp/aliases new file mode 100644 index 000000000..3cf5b97e8 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_snmp/aliases @@ -0,0 +1 @@ +shippable/windows/group3 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_snmp/tasks/cleanup.yml b/ansible_collections/community/windows/tests/integration/targets/win_snmp/tasks/cleanup.yml new file mode 100644 index 000000000..ef446cb73 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_snmp/tasks/cleanup.yml @@ -0,0 +1,16 @@ +--- + - name: Make sure there are no existing SNMP configuration settings + ansible.windows.win_regedit: + path: "{{ item }}" + state: absent + loop: + - "{{ permitted_managers_key }}" + - "{{ valid_communities_key }}" + + - name: Create skeleton registry keys for SNMP + ansible.windows.win_regedit: + path: "{{ item }}" + state: present + loop: + - "{{ permitted_managers_key }}" + - "{{ valid_communities_key }}" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_snmp/tasks/cleanup_using_module.yml b/ansible_collections/community/windows/tests/integration/targets/win_snmp/tasks/cleanup_using_module.yml new file mode 100644 index 000000000..472a03e7e --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_snmp/tasks/cleanup_using_module.yml @@ -0,0 +1,26 @@ +--- + - name: Set no SNMP community or SNMP manager + register: snmp_cleanup + win_snmp: + action: set + community_strings: [] + permitted_managers: [] + + - name: Check registry for no SNMP community + register: snmp_cleanup_reg_community + ansible.windows.win_reg_stat: + path: "{{ valid_communities_key }}" + name: snmp-cleanup + + - name: Check registry for no SNMP manager + register: snmp_cleanup_reg_manager + ansible.windows.win_reg_stat: + path: "{{ permitted_managers_key }}" + name: 1 + + - name: Asset SNMP set operation results in no remaining SNMP details + assert: + that: + - snmp_cleanup.changed + - snmp_cleanup_reg_community.exists == false + - snmp_cleanup_reg_manager.exists == false diff --git a/ansible_collections/community/windows/tests/integration/targets/win_snmp/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_snmp/tasks/main.yml new file mode 100644 index 000000000..09000d6e9 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_snmp/tasks/main.yml @@ -0,0 +1,8 @@ +--- + - include_tasks: cleanup.yml + - include_tasks: snmp_community.yml + - include_tasks: cleanup.yml + - include_tasks: snmp_managers.yml + - include_tasks: output_only.yml + - include_tasks: cleanup_using_module.yml + - include_tasks: cleanup.yml diff --git a/ansible_collections/community/windows/tests/integration/targets/win_snmp/tasks/output_only.yml b/ansible_collections/community/windows/tests/integration/targets/win_snmp/tasks/output_only.yml new file mode 100644 index 000000000..7115da45d --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_snmp/tasks/output_only.yml @@ -0,0 +1,24 @@ +--- + # Already tested + - name: Add an SNMP manager and an SNMP community + win_snmp: + action: add + community_strings: + - snmp-cleanup + permitted_managers: + - 192.168.1.1 + + - name: Run without options + register: snmp_no_options + win_snmp: + + - name: Assert no changes occurred when no options provided + assert: + that: + - not snmp_no_options.changed + + - name: Assert community strings and permitted managers are correctly returned + assert: + that: + - "'snmp-cleanup' in snmp_no_options.community_strings" + - "'192.168.1.1' in snmp_no_options.permitted_managers" diff --git a/ansible_collections/community/windows/tests/integration/targets/win_snmp/tasks/snmp_community.yml b/ansible_collections/community/windows/tests/integration/targets/win_snmp/tasks/snmp_community.yml new file mode 100644 index 000000000..d206d179c --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_snmp/tasks/snmp_community.yml @@ -0,0 +1,165 @@ +--- + - name: Add initial SNMP community + register: snmp_community + win_snmp: + action: add + community_strings: + - ansible-ro-test + + - name: Check initial SNMP community exists in registry + register: snmp_community_reg + ansible.windows.win_reg_stat: + path: "{{ valid_communities_key }}" + name: ansible-ro-test + + - name: Assert initial SNMP community is correct + assert: + that: + - snmp_community is changed + - snmp_community_reg.exists + - snmp_community_reg.type == 'REG_DWORD' + - snmp_community_reg.value == 4 + + - name: Add initial SNMP community again + register: snmp_community_again + win_snmp: + action: add + community_strings: + - ansible-ro-test + + - name: Check no change occurred when adding SNMP community again + assert: + that: + - snmp_community_again is not changed + + - name: Add next SNMP community + register: snmp_community_next + win_snmp: + action: add + community_strings: + - ansible-ro-test-next + + - name: Check initial SNMP community still exists in registry + register: snmp_community_reg_orig + ansible.windows.win_reg_stat: + path: "{{ valid_communities_key }}" + name: ansible-ro-test + + - name: Check next SNMP community exists in registry + register: snmp_community_reg_next + ansible.windows.win_reg_stat: + path: "{{ valid_communities_key }}" + name: ansible-ro-test-next + + - name: Assert initial SNMP community still exists + assert: + that: + - snmp_community_reg_orig.exists + - snmp_community_reg_orig.type == 'REG_DWORD' + - snmp_community_reg_orig.value == 4 + + - name: Assert next SNMP community exists + assert: + that: + - snmp_community_next is changed + - snmp_community_reg_next.exists + - snmp_community_reg_next.type == 'REG_DWORD' + - snmp_community_reg_next.value == 4 + + - name: Replace SNMP community + register: snmp_community_replace + win_snmp: + action: set + community_strings: + - ansible-ro-test-replace + + - name: Check initial SNMP community does not exist in registry + register: snmp_community_reg_orig_replace + ansible.windows.win_reg_stat: + path: "{{ valid_communities_key }}" + name: ansible-ro-test + + - name: Check next SNMP community does not exist in registry + register: snmp_community_reg_next_replace + ansible.windows.win_reg_stat: + path: "{{ valid_communities_key }}" + name: ansible-ro-test-next + + - name: Check replace SNMP community exists in registry + register: snmp_community_reg_replace + ansible.windows.win_reg_stat: + path: "{{ valid_communities_key }}" + name: ansible-ro-test-replace + + - name: Assert replace SNMP community exists and others are replaced + assert: + that: + - snmp_community_replace is changed + - snmp_community_reg_orig_replace.exists == false + - snmp_community_reg_next_replace.exists == false + - snmp_community_reg_replace.exists + - snmp_community_reg_replace.type == 'REG_DWORD' + - snmp_community_reg_replace.value == 4 + + # This task has already been tested + - name: Add another SNMP community before testing removal + win_snmp: + action: add + community_strings: + - ansible-ro-remove-add + + - name: Remove the replaced SNMP community + register: snmp_community_remove + win_snmp: + action: remove + community_strings: + - ansible-ro-test-replace + + - name: Check replace SNMP community is removed in registry + register: snmp_community_reg_remove + ansible.windows.win_reg_stat: + path: "{{ valid_communities_key }}" + name: ansible-ro-test-replace + + - name: Check SNMP community that was added for testing removal exists in registry + register: snmp_community_reg_remove_add + ansible.windows.win_reg_stat: + path: "{{ valid_communities_key }}" + name: ansible-ro-remove-add + + - name: Assert removal of SNMP community succeeded and next SNMP community remains + assert: + that: + - snmp_community_remove is changed + - snmp_community_reg_remove.exists == false + - snmp_community_reg_remove_add.exists + - snmp_community_reg_remove_add.type == 'REG_DWORD' + - snmp_community_reg_remove_add.value == 4 + + - name: Remove the replaced SNMP community (again) + register: snmp_community_remove + win_snmp: + action: remove + community_strings: + - ansible-ro-test-replace + + - name: Check replace SNMP community is removed in registry (again) + register: snmp_community_reg_remove + ansible.windows.win_reg_stat: + path: "{{ valid_communities_key }}" + name: ansible-ro-test-replace + + - name: Check SNMP community that was added for testing removal exists in registry (again) + register: snmp_community_reg_remove_add + ansible.windows.win_reg_stat: + path: "{{ valid_communities_key }}" + name: ansible-ro-remove-add + + - name: Assert removal of SNMP community succeeded and next SNMP community remains (again) + assert: + that: + - snmp_community_remove is not changed + - snmp_community_reg_remove.exists == false + - snmp_community_reg_remove_add.exists + - snmp_community_reg_remove_add.type == 'REG_DWORD' + - snmp_community_reg_remove_add.value == 4 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_snmp/tasks/snmp_managers.yml b/ansible_collections/community/windows/tests/integration/targets/win_snmp/tasks/snmp_managers.yml new file mode 100644 index 000000000..227281d81 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_snmp/tasks/snmp_managers.yml @@ -0,0 +1,158 @@ +--- + - name: Add initial SNMP manager + register: snmp_manager + win_snmp: + action: add + permitted_managers: + - 192.168.1.1 + + - name: Check initial SNMP manager exists in registry + register: snmp_manager_reg + ansible.windows.win_reg_stat: + path: "{{ permitted_managers_key }}" + name: 1 + + - name: Assert initial SNMP manager is correct + assert: + that: + - snmp_manager is changed + - snmp_manager_reg.exists + - snmp_manager_reg.type == 'REG_SZ' + - snmp_manager_reg.value == '192.168.1.1' + + - name: Add initial SNMP manager again + register: snmp_manager_again + win_snmp: + action: add + permitted_managers: + - 192.168.1.1 + + - name: Check no change occurred when adding SNMP manager again + assert: + that: + - snmp_manager_again is not changed + + - name: Add next SNMP manager + register: snmp_manager_next + win_snmp: + action: add + permitted_managers: + - 192.168.1.2 + + - name: Check initial SNMP manager still exists in registry + register: snmp_manager_reg_orig + ansible.windows.win_reg_stat: + path: "{{ permitted_managers_key }}" + name: 1 + + - name: Check next SNMP manager exists in registry + register: snmp_manager_reg_next + ansible.windows.win_reg_stat: + path: "{{ permitted_managers_key }}" + name: 2 + + - name: Assert initial SNMP manager still exists + assert: + that: + - snmp_manager_reg_orig.exists + - snmp_manager_reg_orig.type == 'REG_SZ' + - snmp_manager_reg_orig.value == '192.168.1.1' + + - name: Assert next SNMP manager exists + assert: + that: + - snmp_manager_next is changed + - snmp_manager_reg_next.exists + - snmp_manager_reg_next.type == 'REG_SZ' + - snmp_manager_reg_next.value == '192.168.1.2' + + - name: Replace SNMP manager + register: snmp_manager_replace + win_snmp: + action: set + permitted_managers: + - 192.168.1.10 + + - name: Check next SNMP manager does not exist in registry + register: snmp_manager_reg_next_replace + ansible.windows.win_reg_stat: + path: "{{ permitted_managers_key }}" + name: 2 + + - name: Check replace SNMP manager exists in registry (overrides original slot) + register: snmp_manager_reg_replace + ansible.windows.win_reg_stat: + path: "{{ permitted_managers_key }}" + name: 1 + + - name: Assert replace SNMP manager exists and others are replaced + assert: + that: + - snmp_manager_replace is changed + - snmp_manager_reg_next_replace.exists == false + - snmp_manager_reg_replace.exists + - snmp_manager_reg_replace.type == 'REG_SZ' + - snmp_manager_reg_replace.value == '192.168.1.10' + + # This task has already been tested + - name: Add another SNMP manager before testing removal + win_snmp: + action: add + permitted_managers: + - 192.168.1.20 + + - name: Remove the replaced SNMP manager + register: snmp_manager_remove + win_snmp: + action: remove + permitted_managers: + - 192.168.1.10 + + - name: Check replace SNMP manager is removed in registry + register: snmp_manager_reg_remove + ansible.windows.win_reg_stat: + path: "{{ permitted_managers_key }}" + name: 1 + + - name: Check SNMP manager that was added for testing removal exists in registry + register: snmp_manager_reg_remove_add + ansible.windows.win_reg_stat: + path: "{{ permitted_managers_key }}" + name: 2 + + - name: Assert removal of SNMP manager succeeded and next SNMP manager remains + assert: + that: + - snmp_manager_remove is changed + - snmp_manager_reg_remove.exists == false + - snmp_manager_reg_remove_add.exists + - snmp_manager_reg_remove_add.type == 'REG_SZ' + - snmp_manager_reg_remove_add.value == '192.168.1.20' + + - name: Remove the replaced SNMP manager (again) + register: snmp_manager_remove + win_snmp: + action: remove + permitted_managers: + - 192.168.1.10 + + - name: Check replace SNMP manager is removed in registry (again) + register: snmp_manager_reg_remove + ansible.windows.win_reg_stat: + path: "{{ permitted_managers_key }}" + name: 1 + + - name: Check SNMP manager that was added for testing removal exists in registry (again) + register: snmp_manager_reg_remove_add + ansible.windows.win_reg_stat: + path: "{{ permitted_managers_key }}" + name: 2 + + - name: Assert removal of SNMP manager succeeded and next SNMP manager remains (again) + assert: + that: + - snmp_manager_remove is not changed + - snmp_manager_reg_remove.exists == false + - snmp_manager_reg_remove_add.exists + - snmp_manager_reg_remove_add.type == 'REG_SZ' + - snmp_manager_reg_remove_add.value == '192.168.1.20' diff --git a/ansible_collections/community/windows/tests/integration/targets/win_snmp/vars/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_snmp/vars/main.yml new file mode 100644 index 000000000..610be839f --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_snmp/vars/main.yml @@ -0,0 +1,3 @@ +--- + permitted_managers_key: 'HKLM:\System\CurrentControlSet\services\SNMP\Parameters\PermittedManagers' + valid_communities_key: 'HKLM:\System\CurrentControlSet\services\SNMP\Parameters\ValidCommunities' diff --git a/ansible_collections/community/windows/tests/integration/targets/win_timezone/aliases b/ansible_collections/community/windows/tests/integration/targets/win_timezone/aliases new file mode 100644 index 000000000..215e0b069 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_timezone/aliases @@ -0,0 +1 @@ +shippable/windows/group4 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_timezone/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_timezone/tasks/main.yml new file mode 100644 index 000000000..63f449e46 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_timezone/tasks/main.yml @@ -0,0 +1,19 @@ +- name: Determine if server has tzutil.exe installed + ansible.windows.win_command: tzutil.exe /l + register: tzutil + ignore_errors: yes + +- name: Only run tests if tzutil.exe is installed + when: tzutil.rc == 0 + block: + + - name: Test in normal mode + import_tasks: tests.yml + vars: + in_check_mode: no + + - name: Test in check-mode + import_tasks: tests.yml + vars: + in_check_mode: yes + check_mode: yes diff --git a/ansible_collections/community/windows/tests/integration/targets/win_timezone/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_timezone/tasks/tests.yml new file mode 100644 index 000000000..e03f4f1e1 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_timezone/tasks/tests.yml @@ -0,0 +1,100 @@ +# NOTE: Set to a known starting value, store original +- name: Change starting timezone to GMT + win_timezone: + timezone: GMT Standard Time + register: original + +# NOTE: We don't know if it changed, we don't care +- name: Test GMT timezone + assert: + that: + - original.timezone == 'GMT Standard Time' + +- name: Change timezone to GMT+1 + win_timezone: + timezone: Romance Standard Time + register: romance + +- name: Test GMT+1 timezone + assert: + that: + - romance is changed + - romance.previous_timezone == 'GMT Standard Time' + - romance.timezone == 'Romance Standard Time' + when: not in_check_mode + +- name: Test GMT+1 timezone + assert: + that: + - romance is changed + - romance.previous_timezone == original.timezone + - romance.timezone == 'Romance Standard Time' + when: in_check_mode + +- name: Change timezone to GMT+1 again + win_timezone: + timezone: Romance Standard Time + register: romance + +- name: Test GMT+1 timezone + assert: + that: + - romance is not changed + - romance.previous_timezone == 'Romance Standard Time' + - romance.timezone == 'Romance Standard Time' + when: not in_check_mode + +- name: Test GMT+1 timezone + assert: + that: + - romance is changed + - romance.previous_timezone == original.timezone + - romance.timezone == 'Romance Standard Time' + when: in_check_mode + +- name: Change timezone to GMT+6 + win_timezone: + timezone: Central Standard Time + register: central + +- name: Test GMT-6 timezone + assert: + that: + - central is changed + - central.previous_timezone == 'Romance Standard Time' + - central.timezone == 'Central Standard Time' + when: not in_check_mode + +- name: Test GMT+1 timezone + assert: + that: + - central is changed + - central.previous_timezone == original.timezone + - central.timezone == 'Central Standard Time' + when: in_check_mode + +- name: Change timezone to dstoff + win_timezone: + timezone: Eastern Standard Time_dstoff + register: dstoff_result + +- name: Test dstoff timezone + assert: + that: + - dstoff_result is changed + - dstoff_result.timezone == 'Eastern Standard Time_dstoff' + +- name: Change timezone to GMT+666 + win_timezone: + timezone: Dag's Standard Time + register: dag + ignore_errors: yes + +- name: Test GMT+666 timezone + assert: + that: + - dag is failed + +- name: Restore original timezone + win_timezone: + timezone: '{{ original.timezone }}'
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_toast/aliases b/ansible_collections/community/windows/tests/integration/targets/win_toast/aliases new file mode 100644 index 000000000..ebd7be746 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_toast/aliases @@ -0,0 +1,2 @@ +shippable/windows/group1 +disabled diff --git a/ansible_collections/community/windows/tests/integration/targets/win_toast/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_toast/tasks/main.yml new file mode 100644 index 000000000..735d55b1a --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_toast/tasks/main.yml @@ -0,0 +1,13 @@ +- name: Set up tests + import_tasks: setup.yml + +- name: Test in normal mode + import_tasks: tests.yml + vars: + in_check_mode: no + +- name: Test in check mode + import_tasks: tests.yml + vars: + in_check_mode: yes + check_mode: yes diff --git a/ansible_collections/community/windows/tests/integration/targets/win_toast/tasks/setup.yml b/ansible_collections/community/windows/tests/integration/targets/win_toast/tasks/setup.yml new file mode 100644 index 000000000..869bc5f51 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_toast/tasks/setup.yml @@ -0,0 +1,27 @@ +- name: Get OS version + ansible.windows.win_shell: '[Environment]::OSVersion.Version.Major' + register: os_version + +- name: Get logged in user count (using explorer exe as a proxy) + ansible.windows.win_shell: (get-process -name explorer -EA silentlyContinue).Count + register: user_count + +- name: debug os_version + debug: + var: os_version + verbosity: 2 + +- name: debug user_count + debug: + var: user_count + verbosity: 2 + +- name: Set fact if toast cannot be made + set_fact: + can_toast: False + when: os_version.stdout|int < 10 + +- name: Set fact if toast can be made + set_fact: + can_toast: True + when: os_version.stdout|int >= 10 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_toast/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_toast/tasks/tests.yml new file mode 100644 index 000000000..d1d4ece10 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_toast/tasks/tests.yml @@ -0,0 +1,106 @@ +- name: Warn user + win_toast: + expire: 10 + msg: Keep calm and carry on. + register: msg_result + ignore_errors: True + +- name: Test msg_result when can_toast is true (normal mode, users) + assert: + that: + - msg_result is not failed + - msg_result.time_taken > 10 + when: + - can_toast == True + - in_check_mode == False + - user_count.stdout|int > 0 + +- name: Test msg_result when can_toast is true (normal mode, no users) + assert: + that: + - msg_result is not failed + - msg_result.time_taken > 0.1 + - msg_result.toast_sent == False + when: + - can_toast == True + - in_check_mode == False + - user_count.stdout|int == 0 + +- name: Test msg_result when can_toast is true (check mode, users) + assert: + that: + - msg_result is not failed + - msg_result.time_taken > 0.1 + when: + - can_toast == True + - in_check_mode == True + +- name: Test msg_result when can_toast is true (check mode, no users) + assert: + that: + - msg_result is not failed + - msg_result.time_taken > 0.1 + - msg_result.toast_sent == False + when: + - can_toast == True + - in_check_mode == True + - user_count.stdout|int == 0 + +- name: Test msg_result when can_toast is false + assert: + that: + - msg_result is failed + when: can_toast == False + +- name: Warn user again + win_toast: + expire: 10 + msg: Keep calm and carry on. + register: msg_result2 + ignore_errors: True + +- name: Test msg_result2 when can_toast is true (normal mode, users) + assert: + that: + - msg_result2 is not failed + - msg_result2.time_taken > 10 + when: + - can_toast == True + - in_check_mode == False + - user_count.stdout|int > 0 + +- name: Test msg_result2 when can_toast is true (normal mode, no users) + assert: + that: + - msg_result2 is not failed + - msg_result2.time_taken > 0.1 + when: + - can_toast == True + - in_check_mode == False + - user_count.stdout|int == 0 + +- name: Test msg_result2 when can_toast is true (check mode, users) + assert: + that: + - msg_result2 is not failed + - msg_result2.time_taken > 0.1 + when: + - can_toast == True + - in_check_mode == False + - user_count.stdout|int > 0 + +- name: Test msg_result2 when can_toast is true (check mode, no users) + assert: + that: + - msg_result2 is not failed + - msg_result2.time_taken > 0.1 + when: + - can_toast == True + - in_check_mode == False + - user_count.stdout|int == 0 + +- name: Test msg_result2 when can_toast is false + assert: + that: + - msg_result2 is failed + when: can_toast == False diff --git a/ansible_collections/community/windows/tests/integration/targets/win_unzip/aliases b/ansible_collections/community/windows/tests/integration/targets/win_unzip/aliases new file mode 100644 index 000000000..4f4664b68 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_unzip/aliases @@ -0,0 +1 @@ +shippable/windows/group5 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_unzip/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_unzip/defaults/main.yml new file mode 100644 index 000000000..52d808d88 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_unzip/defaults/main.yml @@ -0,0 +1 @@ +win_unzip_dir: '{{ remote_tmp_dir }}\win_unzip .ÅÑŚÌβŁÈ [$!@^&test(;)]' diff --git a/ansible_collections/community/windows/tests/integration/targets/win_unzip/files/create_crafty_zip_files.py b/ansible_collections/community/windows/tests/integration/targets/win_unzip/files/create_crafty_zip_files.py new file mode 100644 index 000000000..8845b4862 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_unzip/files/create_crafty_zip_files.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 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 + +import os +import shutil +import sys +import zipfile + +# Each key is a zip file and the vaule is the list of files that will be created +# and placed in the archive +zip_files = { + 'hat1': [r'hat/..\rabbit.txt'], + 'hat2': [r'hat/..\..\rabbit.txt'], + 'handcuffs': [r'..\..\houidini.txt'], + 'prison': [r'..\houidini.txt'], +} + +# Accept an argument of where to create the files, defaulting to +# the current working directory. +try: + output_dir = sys.argv[1] +except IndexError: + output_dir = os.getcwd() + +if not os.path.isdir(output_dir): + os.mkdir(output_dir) + +os.chdir(output_dir) + +for name, files in zip_files.items(): + # Create the files to go in the zip archive + for entry in files: + dirname = os.path.dirname(entry) + if dirname: + if os.path.isdir(dirname): + shutil.rmtree(dirname) + os.mkdir(dirname) + + with open(entry, 'w') as e: + e.write('escape!\n') + + # Create the zip archive with the files + filename = '%s.zip' % name + if os.path.isfile(filename): + os.unlink(filename) + + with zipfile.ZipFile(filename, 'w') as zf: + for entry in files: + zf.write(entry) + + # Cleanup + if dirname: + shutil.rmtree(dirname) + + for entry in files: + try: + os.unlink(entry) + except OSError: + pass diff --git a/ansible_collections/community/windows/tests/integration/targets/win_unzip/files/create_zip.py b/ansible_collections/community/windows/tests/integration/targets/win_unzip/files/create_zip.py new file mode 100644 index 000000000..41b6ff068 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_unzip/files/create_zip.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, 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 + +import sys +import tempfile +import zipfile + + +def main(): + filename = b"caf\xc3\xa9.txt" + + with tempfile.NamedTemporaryFile() as temp: + with open(temp.name, mode="wb") as fd: + fd.write(filename) + + with open(sys.argv[1], mode="wb") as fd: + with zipfile.ZipFile(fd, "w") as zip: + zip.write(temp.name, filename.decode('utf-8')) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/windows/tests/integration/targets/win_unzip/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_unzip/meta/main.yml new file mode 100644 index 000000000..9f37e96cd --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_unzip/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_remote_tmp_dir diff --git a/ansible_collections/community/windows/tests/integration/targets/win_unzip/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_unzip/tasks/main.yml new file mode 100644 index 000000000..82054d1f4 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_unzip/tasks/main.yml @@ -0,0 +1,171 @@ +- name: create test directory + ansible.windows.win_file: + path: '{{ win_unzip_dir }}\output' + state: directory + +- name: create local zip file with non-ascii chars + script: create_zip.py {{ output_dir + '/win_unzip.zip' | quote }} + delegate_to: localhost + +- name: copy across zip to Windows host + ansible.windows.win_copy: + src: '{{ output_dir }}/win_unzip.zip' + dest: '{{ win_unzip_dir }}\win_unzip.zip' + +- name: unarchive zip (check) + win_unzip: + src: '{{ win_unzip_dir }}\win_unzip.zip' + dest: '{{ win_unzip_dir }}\output' + register: unzip_check + check_mode: yes + +- name: get result of unarchive zip (check) + ansible.windows.win_stat: + path: '{{ win_unzip_dir }}\output\café.txt' + register: unzip_actual_check + +- name: assert result of unarchive zip (check) + assert: + that: + - unzip_check is changed + - not unzip_check.removed + - not unzip_actual_check.stat.exists + +- name: unarchive zip + win_unzip: + src: '{{ win_unzip_dir }}\win_unzip.zip' + dest: '{{ win_unzip_dir }}\output' + register: unzip + +- name: get result of unarchive zip + ansible.windows.slurp: + path: '{{ win_unzip_dir }}\output\café.txt' + register: unzip_actual + +- name: assert result of unarchive zip + assert: + that: + - unzip is changed + - not unzip.removed + - unzip_actual.content | b64decode == 'café.txt' + +# Module is not idempotent, will always change without creates +- name: unarchive zip again without creates + win_unzip: + src: '{{ win_unzip_dir }}\win_unzip.zip' + dest: '{{ win_unzip_dir }}\output' + register: unzip_again + +- name: assert unarchive zip again without creates + assert: + that: + - unzip_again is changed + - not unzip_again.removed + +- name: unarchive zip with creates + win_unzip: + src: '{{ win_unzip_dir }}\win_unzip.zip' + dest: '{{ win_unzip_dir }}\outout' + creates: '{{ win_unzip_dir }}\output\café.txt' + register: unzip_again_creates + +- name: assert unarchive zip with creates + assert: + that: + - not unzip_again_creates is changed + - not unzip_again_creates.removed + +- name: unarchive zip with delete (check) + win_unzip: + src: '{{ win_unzip_dir }}\win_unzip.zip' + dest: '{{ win_unzip_dir }}\output' + delete_archive: yes + register: unzip_delete_check + check_mode: yes + +- name: get result of unarchive zip with delete (check) + ansible.windows.win_stat: + path: '{{ win_unzip_dir }}\win_unzip.zip' + register: unzip_delete_actual_check + +- name: assert unarchive zip with delete (check) + assert: + that: + - unzip_delete_check is changed + - unzip_delete_check.removed + - unzip_delete_actual_check.stat.exists + +- name: unarchive zip with delete + win_unzip: + src: '{{ win_unzip_dir }}\win_unzip.zip' + dest: '{{ win_unzip_dir }}\output' + delete_archive: yes + register: unzip_delete + +- name: get result of unarchive zip with delete + ansible.windows.win_stat: + path: '{{ win_unzip_dir }}\win_unzip.zip' + register: unzip_delete_actual + +- name: assert unarchive zip with delete + assert: + that: + - unzip_delete is changed + - unzip_delete.removed + - not unzip_delete_actual.stat.exists + +# Path traversal tests (CVE-2020-1737) +- name: Create zip files + script: create_crafty_zip_files.py {{ output_dir }} + delegate_to: localhost + +- name: Copy zip files to Windows host + ansible.windows.win_copy: + src: "{{ output_dir }}/{{ item }}.zip" + dest: "{{ win_unzip_dir }}/" + loop: + - hat1 + - hat2 + - handcuffs + - prison + +- name: Perform first trick + win_unzip: + src: '{{ win_unzip_dir }}\hat1.zip' + dest: '{{ win_unzip_dir }}\output' + register: hat_trick1 + +- name: Check for file + ansible.windows.win_stat: + path: '{{ win_unzip_dir }}\output\rabbit.txt' + register: rabbit + +- name: Perform next tricks (which should all fail) + win_unzip: + src: '{{ win_unzip_dir }}\{{ item }}.zip' + dest: '{{ win_unzip_dir }}\output' + ignore_errors: yes + register: escape + loop: + - hat2 + - handcuffs + - prison + +- name: Search for files + ansible.windows.win_find: + recurse: yes + paths: + - '{{ win_unzip_dir }}' + patterns: + - '*houdini.txt' + - '*rabbit.txt' + register: files + +- name: Check results + assert: + that: + - rabbit.stat.exists + - hat_trick1 is success + - escape.results | map(attribute='failed') | unique | list == [True] + - files.matched == 1 + - files.files[0]['filename'] == 'rabbit.txt' diff --git a/ansible_collections/community/windows/tests/integration/targets/win_user_profile/aliases b/ansible_collections/community/windows/tests/integration/targets/win_user_profile/aliases new file mode 100644 index 000000000..215e0b069 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_user_profile/aliases @@ -0,0 +1 @@ +shippable/windows/group4 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_user_profile/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_user_profile/tasks/main.yml new file mode 100644 index 000000000..67a9bccc9 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_user_profile/tasks/main.yml @@ -0,0 +1,42 @@ +--- +- name: set custom user facts + set_fact: + test_username: ansible_test + test_password: '{{ "password123!" + lookup("password", "/dev/null chars=ascii_letters,digits length=9") }}' + +- name: create test account + ansible.windows.win_user: + name: '{{ test_username }}' + password: '{{ test_password }}' + state: present + register: test_username_info + +- block: + - name: check if profile exists + ansible.windows.win_stat: + path: C:\temp\{{ test_username }} + register: profile_path + + - name: assert that profile doesn't exist before the test + assert: + that: + - not profile_path.stat.exists + + - name: run tests + include_tasks: tests.yml + + always: + - name: remove test account + ansible.windows.win_user: + name: '{{ test_username }}' + state: absent + + - name: remove test account profile + win_user_profile: + name: '{{ item }}' + state: absent + remove_multiple: True + with_items: + - '{{ test_username }}' + - '{{ test_username }}.000' + - test_username_profile diff --git a/ansible_collections/community/windows/tests/integration/targets/win_user_profile/tasks/tests.yml b/ansible_collections/community/windows/tests/integration/targets/win_user_profile/tasks/tests.yml new file mode 100644 index 000000000..52e8754b9 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_user_profile/tasks/tests.yml @@ -0,0 +1,374 @@ +--- +- name: create profile (check mode) + win_user_profile: + username: '{{ test_username }}' + state: present + register: create_profile_check + check_mode: True + +- name: check if profile was created (check mode) + ansible.windows.win_stat: + path: C:\Users\{{ test_username }} + register: create_profile_actual_check + +- name: assert create profile (check mode) + assert: + that: + - create_profile_check is changed + - create_profile_check.path|lower == "c:\\users\\" + test_username + - not create_profile_actual_check.stat.exists + +- name: create profile + win_user_profile: + username: '{{ test_username }}' + state: present + register: create_profile + +- name: check if profile was created + ansible.windows.win_stat: + path: C:\Users\{{ test_username }} + register: create_profile_actual + +- name: assert create profile + assert: + that: + - create_profile is changed + - create_profile.path|lower == "c:\\users\\" + test_username + - create_profile_actual.stat.exists + +- name: create profile (idempotent) + win_user_profile: + username: '{{ test_username }}' + state: present + register: create_profile_again + +- name: assert create profile (idempotent) + assert: + that: + - not create_profile_again is changed + - create_profile_again.path|lower == "c:\\users\\" + test_username + +- name: remove profile (check mode) + win_user_profile: + username: '{{ test_username }}' + state: absent + register: remove_profile_check + check_mode: True + +- name: check if profile was removed (check mode) + ansible.windows.win_stat: + path: C:\Users\{{ test_username }} + register: remove_profile_actual_check + +- name: assert remove profile (check mode) + assert: + that: + - remove_profile_check is changed + - remove_profile_check.path|lower == "c:\\users\\" + test_username + - remove_profile_actual_check.stat.exists + +- name: remove profile + win_user_profile: + username: '{{ test_username }}' + state: absent + register: remove_profile + +- name: check if profile was removed + ansible.windows.win_stat: + path: C:\Users\{{ test_username }} + register: remove_profile_actual + +- name: assert remove profile + assert: + that: + - remove_profile is changed + - remove_profile.path|lower == "c:\\users\\" + test_username + - not remove_profile_actual.stat.exists + +- name: remove profile (idempotent) + win_user_profile: + username: '{{ test_username }}' + state: absent + register: remove_profile_again + +- name: assert remove profile (idempotent) + assert: + that: + - not remove_profile_again is changed + - remove_profile_again.path == None + +- name: create profile with specific base path + win_user_profile: + username: '{{ test_username }}' + name: test_username_profile + state: present + register: create_profile_basename + +- name: check if profile with specific base path was created + ansible.windows.win_stat: + path: C:\Users\test_username_profile + register: create_profile_basename_actual + +- name: assert create profile with specific base path + assert: + that: + - create_profile_basename is changed + - create_profile_basename.path|lower == "c:\\users\\test_username_profile" + - create_profile_basename_actual.stat.exists + +- name: remove profile with specific base path + win_user_profile: + username: '{{ test_username }}' + state: absent + register: remove_profile_basename + +- name: check if profile with specific base path was removed + ansible.windows.win_stat: + path: C:\Users\test_username_profile + register: remove_profile_basename_actual + +- name: assert remove profile with specific base path + assert: + that: + - remove_profile_basename is changed + - remove_profile_basename.path|lower == "c:\\users\\test_username_profile" + - not remove_profile_basename_actual.stat.exists + +- name: create dummy profile folder + ansible.windows.win_file: + path: C:\Users\{{ test_username }} + state: directory + +- block: + - name: create profile folder with conflict (check mode) + win_user_profile: + username: '{{ test_username }}' + state: present + register: create_profile_conflict_check + check_mode: True + + - name: get result of create profile folder with conflict (check mode) + ansible.windows.win_stat: + path: C:\Users\{{ test_username }}.000 + register: create_profile_conflict_actual_check + + - name: assert create profile folder with conflict (check mode) + assert: + that: + - create_profile_conflict_check is changed + # The check mode path calc is dumb, doesn't check for conflicts + - create_profile_conflict_check.path|lower == "c:\\users\\" + test_username + - not create_profile_conflict_actual_check.stat.exists + + - name: create profile folder with conflict + win_user_profile: + username: '{{ test_username }}' + state: present + register: create_profile_conflict + + - name: get result of create profile with conflict + ansible.windows.win_stat: + path: C:\Users\{{ test_username }}.000 + register: create_profile_conflict_actual + + - name: assert create profile folder with conflict + assert: + that: + - create_profile_conflict is changed + - create_profile_conflict.path|lower == "c:\\users\\" + test_username + ".000" + - create_profile_conflict_actual.stat.exists + + - name: remove profile with conflict + win_user_profile: + username: '{{ test_username }}' + state: absent + register: remove_profile_conflict + + - name: get result of profile folder after remove + ansible.windows.win_stat: + path: C:\Users\{{ test_username }}.000 + register: remove_profile_conflict_actual + + - name: get result of dummy folder after remove + ansible.windows.win_stat: + path: C:\Users\{{ test_username }} + register: remove_profile_conflict_dummy + + - name: assert remove profile with conflict + assert: + that: + - remove_profile_conflict is changed + - remove_profile_conflict.path|lower == "c:\\users\\" + test_username + ".000" + - not remove_profile_conflict_actual.stat.exists + - remove_profile_conflict_dummy.stat.exists + + always: + - name: remove dummy profile folder + ansible.windows.win_file: + path: C:\Users\{{ test_username }} + state: absent + +- name: create profile for deleted user by sid test + win_user_profile: + username: '{{ test_username_info.sid }}' + state: present + +- name: delete user for deleted user with sid test + ansible.windows.win_user: + name: '{{ test_username }}' + state: absent + +- name: remove profile for remove profile by sid test + win_user_profile: + username: '{{ test_username_info.sid }}' + state: absent + register: remove_profile_deleted_sid + +- name: check if profile was deleted for deleted user using a SID + ansible.windows.win_stat: + path: C:\Users\{{ test_username }} + register: remove_profile_deleted_sid_actual + +- name: assert remove profile for deleted user using a SID + assert: + that: + - remove_profile_deleted_sid is changed + - remove_profile_deleted_sid.path|lower == "c:\\users\\" + test_username + - not remove_profile_deleted_sid_actual.stat.exists + +- name: recreate user for deleted user by name test + ansible.windows.win_user: + name: '{{ test_username }}' + password: '{{ test_password }}' + state: present + register: test_orphan_user1 + +- name: create profile for deleted user by name test + win_user_profile: + username: '{{ test_username }}' + state: present + +- name: delete user for remove profile by name test + ansible.windows.win_user: + name: '{{ test_username }}' + state: absent + +- name: remove profile for deleted user using a name + win_user_profile: + name: '{{ test_username }}' + state: absent + register: remove_profile_deleted_name + +- name: check if profile was deleted for deleted user using a name + ansible.windows.win_stat: + path: C:\Users\{{ test_username }} + register: remove_profile_deleted_name_actual + +- name: assert remove profile for deleted user using a name + assert: + that: + - remove_profile_deleted_name is changed + - remove_profile_deleted_name.path|lower == "c:\\users\\" + test_username + - not remove_profile_deleted_name_actual.stat.exists + +- name: remove profile for deleted user using a name (idempotent) + win_user_profile: + name: '{{ test_username }}' + state: absent + register: remove_profile_deleted_name_again + +- name: assert remove profile for deleted user using a name (idempotent) + assert: + that: + - not remove_profile_deleted_name_again is changed + +- name: recreate user for remove multiple user test + ansible.windows.win_user: + name: '{{ test_username }}' + password: '{{ test_password }}' + state: present + register: test_orphan_user1 + +- name: create new profile for remove multiple user test + win_user_profile: + username: '{{ test_username }}' + state: present + register: orphan_user1_profile + +- name: remove user 1 for remove multiple user test + ansible.windows.win_user: + name: '{{ test_username }}' + state: absent + +# win_file has issues with paths exceeding MAX_PATH, need to use rmdir instead +- name: remove profile folder for user 1 + ansible.windows.win_shell: rmdir /S /Q {{ orphan_user1_profile.path}} + args: + executable: cmd.exe + +- name: create user 2 for remove multiple user test + ansible.windows.win_user: + name: '{{ test_username }}' + password: '{{ test_password }}' + state: present + register: test_orphan_user2 + +- name: create new profile for orphan user 2 + win_user_profile: + username: '{{ test_username }}' + state: present + register: orphan_user2_profile + +- name: remove orphan user 2 for remove multiple user test + ansible.windows.win_user: + name: '{{ test_username }}' + state: present + +- name: fail to remove multiple profiles without flag + win_user_profile: + name: '{{ test_username }}' + state: absent + register: fail_remove_multiple + ignore_errors: True + +- name: check if profile was removed + ansible.windows.win_stat: + path: C:\Users\{{ test_username }} + register: fail_remove_multiple_actual + +- name: assert that profile was not actually deleted + assert: + that: + - fail_remove_multiple.msg == "Found multiple profiles matching the path 'C:\\Users\\" + test_username + "', set 'remove_multiple=True' to remove all the profiles for this match" + - fail_remove_multiple_actual.stat.exists + +- name: remove multiple profiles + win_user_profile: + name: '{{ test_username }}' + state: absent + remove_multiple: True + register: remove_multiple + +- name: get result of remove multiple profiles + ansible.windows.win_stat: + path: C:\Users\{{ test_username }} + register: remove_multiple_actual + +- name: check that orphan user 1 reg profile has been removed + ansible.windows.win_reg_stat: + path: HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\{{ test_orphan_user1.sid }} + register: remove_orphan1_actual + +- name: check that orphan user 2 reg profile has been removed + ansible.windows.win_reg_stat: + path: HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\{{ test_orphan_user2.sid }} + register: remove_orphan2_actual + +- name: assert remove multiple profiles + assert: + that: + - remove_multiple is changed + - remove_multiple.path|lower == "c:\\users\\" + test_username + - not remove_multiple_actual.stat.exists + - not remove_orphan1_actual.exists + - not remove_orphan2_actual.exists diff --git a/ansible_collections/community/windows/tests/integration/targets/win_wait_for_process/aliases b/ansible_collections/community/windows/tests/integration/targets/win_wait_for_process/aliases new file mode 100644 index 000000000..215e0b069 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_wait_for_process/aliases @@ -0,0 +1 @@ +shippable/windows/group4 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_wait_for_process/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_wait_for_process/tasks/main.yml new file mode 100644 index 000000000..3ec692cb4 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_wait_for_process/tasks/main.yml @@ -0,0 +1,201 @@ +--- +- name: Get powershell version + ansible.windows.win_shell: $PSVersionTable.PSVersion.Major + register: powershell_version + +- name: Ensure Spooler service is started + ansible.windows.win_service: + name: Spooler + state: started + +- name: Wait for non-existing process to not exist + win_wait_for_process: + process_name_exact: + - ansible_foobar + timeout: 30 + state: absent + register: absent_nonexisting_process + +- assert: + that: + - absent_nonexisting_process is success + - absent_nonexisting_process is not changed + - absent_nonexisting_process.elapsed > 0 + - absent_nonexisting_process.elapsed < 30 + - absent_nonexisting_process.matched_processes|length == 0 + +- name: Wait for non-existing process until timeout + win_wait_for_process: + process_name_exact: ansible_foobar + timeout: 30 + state: present + ignore_errors: yes + register: present_nonexisting_process + +- assert: + that: + - present_nonexisting_process is failed + - present_nonexisting_process is not changed + - present_nonexisting_process.elapsed > 30 + - present_nonexisting_process.msg == 'Timed out while waiting for process(es) to start' + - present_nonexisting_process.matched_processes|length == 0 + +- name: Wait for existing process to exist + win_wait_for_process: + process_name_exact: spoolsv + timeout: 30 + state: present + register: present_existing_process + +- assert: + that: + - present_existing_process is success + - present_existing_process is not changed + - present_existing_process.elapsed > 0 + - present_existing_process.elapsed < 30 + - present_existing_process.matched_processes|length > 0 + +- name: Wait for existing process until timeout + win_wait_for_process: + process_name_exact: + - spoolsv + timeout: 30 + state: absent + ignore_errors: yes + register: absent_existing_process + +- assert: + that: + - absent_existing_process is failed + - absent_existing_process is not changed + - absent_existing_process.elapsed > 30 + - absent_existing_process.matched_processes|length > 0 + - absent_existing_process.msg == 'Timeout while waiting for process(es) to stop' + +- name: Wait for existing process to exist (using owner) + win_wait_for_process: + process_name_exact: spoolsv + owner: SYSTEM + timeout: 30 + state: present + ignore_errors: yes + register: present_existing_owner_process + +- assert: + that: + - present_existing_owner_process is success + - present_existing_owner_process is not changed + - present_existing_owner_process.elapsed > 0 + - present_existing_owner_process.elapsed < 30 + - present_existing_owner_process.matched_processes|length > 0 + when: powershell_version.stdout_lines[0]|int >= 4 + +- assert: + that: + - present_existing_owner_process is failed + - present_existing_owner_process is not changed + - present_existing_owner_process.elapsed == 0 + - present_existing_owner_process.matched_processes|length == 0 + - present_existing_owner_process.msg == "This version of Powershell does not support filtering processes by 'owner'." + when: powershell_version.stdout_lines[0]|int < 4 + +- name: Wait for Spooler service to stop + win_wait_for_process: + process_name_exact: + - spoolsv + - other_process # Tests that just 1 needs to match + timeout: 60 + state: absent + async: 30 + poll: 0 + register: spoolsv_process + +- name: Stop the Spooler service + ansible.windows.win_service: + name: Spooler + force_dependent_services: yes + state: stopped + +- name: Check on async task + async_status: + jid: '{{ spoolsv_process.ansible_job_id }}' + until: absent_spoolsv_process is finished + retries: 20 + register: absent_spoolsv_process + +- assert: + that: + - absent_spoolsv_process is success + - absent_spoolsv_process is not changed + - absent_spoolsv_process is finished + - absent_spoolsv_process.elapsed > 0 + - absent_spoolsv_process.elapsed < 30 + - absent_spoolsv_process.matched_processes|length == 1 + +- name: Wait for Spooler service to start + win_wait_for_process: + process_name_exact: spoolsv + timeout: 60 + state: present + async: 60 + poll: 0 + register: spoolsv_process + +- name: Start the spooler service + ansible.windows.win_service: + name: Spooler + force_dependent_services: yes + state: started + +- name: Check on async task + async_status: + jid: '{{ spoolsv_process.ansible_job_id }}' + until: present_spoolsv_process is finished + retries: 10 + register: present_spoolsv_process + +- assert: + that: + - present_spoolsv_process is success + - present_spoolsv_process is not changed + - present_spoolsv_process is finished + - present_spoolsv_process.elapsed > 0 + - present_spoolsv_process.elapsed < 60 + - present_spoolsv_process.matched_processes|length == 1 + +- name: Start a new long-running process + ansible.windows.win_shell: | + Start-Sleep -Seconds 15 + async: 40 + poll: 0 + register: sleep_pid + +- name: Wait for PID to start + win_wait_for_process: + pid: '{{ sleep_pid.ansible_async_watchdog_pid }}' + timeout: 20 + state: present + register: present_sleep_pid + +- assert: + that: + - present_sleep_pid is success + - present_sleep_pid is not changed + - present_sleep_pid.elapsed > 0 + - present_sleep_pid.elapsed < 15 + - present_sleep_pid.matched_processes|length == 1 + +- name: Wait for PID to stop + win_wait_for_process: + pid: '{{ sleep_pid.ansible_async_watchdog_pid }}' + timeout: 20 + state: absent + register: absent_sleep_pid + +- assert: + that: + - absent_sleep_pid is success + - absent_sleep_pid is not changed + - absent_sleep_pid.elapsed > 0 + - absent_sleep_pid.elapsed < 15 + - absent_sleep_pid.matched_processes|length == 1 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_wakeonlan/aliases b/ansible_collections/community/windows/tests/integration/targets/win_wakeonlan/aliases new file mode 100644 index 000000000..3cf5b97e8 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_wakeonlan/aliases @@ -0,0 +1 @@ +shippable/windows/group3 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_wakeonlan/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_wakeonlan/tasks/main.yml new file mode 100644 index 000000000..169362b00 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_wakeonlan/tasks/main.yml @@ -0,0 +1,9 @@ +- name: Send a magic Wake-on-LAN packet to 00:00:5E:00:53:66 + win_wakeonlan: + mac: 00:00:5E:00:53:66 + broadcast: 192.0.2.255 + +- name: Send a magic Wake-On-LAN packet on port 9 to 00-00-5E-00-53-66 + win_wakeonlan: + mac: 00-00-5E-00-53-66 + port: 9 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_xml/aliases b/ansible_collections/community/windows/tests/integration/targets/win_xml/aliases new file mode 100644 index 000000000..3cf5b97e8 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_xml/aliases @@ -0,0 +1 @@ +shippable/windows/group3 diff --git a/ansible_collections/community/windows/tests/integration/targets/win_xml/files/books.xml b/ansible_collections/community/windows/tests/integration/targets/win_xml/files/books.xml new file mode 100644 index 000000000..e38ee15d4 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_xml/files/books.xml @@ -0,0 +1,10 @@ +<?xml version='1.0' encoding='utf-8'?> +<books> + <works lang="en"> + <title lang="en" isbn13="123412341234X">A Great Book</title> + <title lang="en" isbn13="1234109823400">Best Book Ever</title> + <title lang="en" isbn13="123412121234X">Worst Book Ever</title> + <title lang="en" isbn13="423412341234X">Another Book</title> + <title lang="en" isbn13="523412341234X">Worst Book Ever Two</title> + </works> +</books> diff --git a/ansible_collections/community/windows/tests/integration/targets/win_xml/files/config.xml b/ansible_collections/community/windows/tests/integration/targets/win_xml/files/config.xml new file mode 100644 index 000000000..b5241b351 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_xml/files/config.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<config> + <string key="foo">bar</string> + <setting></setting> +</config> diff --git a/ansible_collections/community/windows/tests/integration/targets/win_xml/files/log4j.xml b/ansible_collections/community/windows/tests/integration/targets/win_xml/files/log4j.xml new file mode 100644 index 000000000..54b76cf7f --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_xml/files/log4j.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> +<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" debug="false"> + <appender name="stdout" class="org.apache.log4j.ConsoleAppender" > + <layout class="org.apache.log4j.PatternLayout"> + <param name="ConversionPattern" value="[%d{dd/MM/yy hh:mm:ss:sss z}] %5p %c{2}: %m%n"/> + </layout> + </appender> + + <appender name="file" class="org.apache.log4j.DailyRollingFileAppender"> + <param name="append" value="true" /> + <param name="encoding" value="UTF-8" /> + <param name="file" value="mylogfile.log" /> + <param name="DatePattern" value="'.'yyyy-MM-dd" /> + <layout class="org.apache.log4j.PatternLayout"> + <param name="ConversionPattern" value="[%-25d{ISO8601}] %-5p %x %C{1} -- %m\n" /> + </layout> + </appender> + + <logger name="org.springframework.security.web.FilterChainProxy" additivity="false"> + <level value="error"/> + <appender-ref ref="file" /> + </logger> + + <logger name="org.springframework.security.web.context.HttpSessionSecurityContextRepository" additivity="false"> + <level value="error"/> + <appender-ref ref="file" /> + </logger> + + <logger name="org.springframework.security.web.context.SecurityContextPersistenceFilter" additivity="false"> + <level value="error"/> + <appender-ref ref="file" /> + </logger> + + <logger name="org.springframework.security.web.access.intercept" additivity="false"> + <level value="error"/> + <appender-ref ref="stdout" /> + </logger> + + <logger name="org.apache.commons.digester" additivity="false"> + <level value="info"/> + <appender-ref ref="stdout" /> + </logger> + + <root> + <priority value="debug"/> + <appender-ref ref="stdout"/> + </root> +</log4j:configuration> diff --git a/ansible_collections/community/windows/tests/integration/targets/win_xml/files/plane.zip b/ansible_collections/community/windows/tests/integration/targets/win_xml/files/plane.zip Binary files differnew file mode 100644 index 000000000..8157182aa --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_xml/files/plane.zip diff --git a/ansible_collections/community/windows/tests/integration/targets/win_xml/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_xml/meta/main.yml new file mode 100644 index 000000000..9f37e96cd --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_xml/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_remote_tmp_dir diff --git a/ansible_collections/community/windows/tests/integration/targets/win_xml/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_xml/tasks/main.yml new file mode 100644 index 000000000..b51dc563f --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_xml/tasks/main.yml @@ -0,0 +1,361 @@ +# test code for the Windows xml module +# (c) 2017, Richard Levenberg <richard.levenberg@cosocloud.com> + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +- name: copy a test .xml file + ansible.windows.win_copy: + src: config.xml + dest: "{{ remote_tmp_dir }}\\config.xml" + +- name: add an element that only has a text child node + win_xml: + path: "{{ remote_tmp_dir }}\\config.xml" + fragment: '<string key="answer">42</string>' + xpath: '/config' + register: element_add_result + +- name: check element add result + assert: + that: + - element_add_result is changed + +- name: try to add the element that only has a text child node again + win_xml: + path: "{{ remote_tmp_dir }}\\config.xml" + fragment: '<string key="answer">42</string>' + xpath: '/config' + register: element_add_result_second + +- name: check element add result + assert: + that: + - not element_add_result_second is changed + +- name: copy a test log4j.xml + ansible.windows.win_copy: + src: log4j.xml + dest: "{{ remote_tmp_dir }}\\log4j.xml" + +- name: change an attribute to fatal logging + win_xml: + path: "{{ remote_tmp_dir }}\\log4j.xml" + xpath: '/log4j:configuration/logger[@name="org.apache.commons.digester"]/level' + type: attribute + attribute: 'value' + fragment: 'FATAL' + +- name: try to change the attribute again + win_xml: + path: "{{ remote_tmp_dir }}\\log4j.xml" + xpath: '/log4j:configuration/logger[@name="org.apache.commons.digester"]/level' + type: attribute + attribute: 'value' + fragment: 'FATAL' + register: attribute_changed_result + +- name: check attribute change result + assert: + that: + - attribute_changed_result is not changed + +- name: try to add a new attribute + win_xml: + path: "{{ remote_tmp_dir }}\\config.xml" + xpath: '/config/string[@key="foo"]' + type: attribute + attribute: spam + fragment: 'ham' + register: element_add_attribute_result + +- name: check element add attribute result + assert: + that: + - element_add_attribute_result is changed + +- name: try to set the added attribute to the same value again + win_xml: + path: "{{ remote_tmp_dir }}\\config.xml" + xpath: '/config/string[@key="foo"]' + type: attribute + attribute: spam + fragment: 'ham' + register: element_add_attribute_result_second + +- name: check element add attribute result + assert: + that: + - not element_add_attribute_result_second is changed + +- name: try to add a new element in empty element + win_xml: + path: "{{ remote_tmp_dir }}\\config.xml" + xpath: '/config/setting' + fragment: "<property />" + type: element + register: element_add_empty_element + +- name: check element add in empty element result + assert: + that: + - element_add_empty_element is changed + +- name: try to add a new element in empty element again + win_xml: + path: "{{ remote_tmp_dir }}\\config.xml" + xpath: '/config/setting' + fragment: "<property />" + type: element + register: element_add_empty_element_second + +- name: check element add in empty element result + assert: + that: + - not element_add_empty_element_second is changed + +# This testing is for https://github.com/ansible/ansible/issues/48471 +# The issue was that an .xml with no encoding declaration, but a UTF8 BOM +# with some UTF-8 characters was being written out with garbage characters. +# The characters added by win_xml were not UTF-8 characters. + +- name: copy test files (https://github.com/ansible/ansible/issues/48471) + ansible.windows.win_copy: + src: plane.zip + dest: "{{ remote_tmp_dir }}\\plane.zip" + +- name: unarchive the test files + win_unzip: + src: "{{ remote_tmp_dir }}\\plane.zip" + dest: "{{ remote_tmp_dir }}\\" + +- name: change a text value in a file with UTF8 BOM and armenian characters in the description + win_xml: + path: "{{ remote_tmp_dir }}\\plane-utf8-bom-armenian-characters.xml" + xpath: '/plane/year' + type: text + fragment: '1988' + +- name: register the sha1 of the new file + ansible.windows.win_stat: + path: "{{ remote_tmp_dir }}\\plane-utf8-bom-armenian-characters.xml" + get_checksum: yes + register: sha1_checksum + +- name: verify the checksum + assert: + that: + - sha1_checksum.stat.checksum == 'e3e18c3066e1bfce9a5cf87c81353fa174440944' + +- name: change a text value in a file with UTF8 BOM and armenian characters in the description + win_xml: + path: "{{ remote_tmp_dir }}\\plane-utf8-bom-armenian-characters.xml" + xpath: '/plane/year' + type: text + fragment: '1989' + backup: yes + register: test_backup + +- name: check backup_file + ansible.windows.win_stat: + path: '{{ test_backup.backup_file }}' + register: backup_file + +- name: Check backup_file + assert: + that: + - test_backup is changed + - backup_file.stat.exists == true + +- name: change a text value in a file with UTF-16 BE BOM and Chinese characters in the description + win_xml: + path: "{{ remote_tmp_dir }}\\plane-utf16be-bom-chinese-characters.xml" + xpath: '/plane/year' + type: text + fragment: '1988' + +- name: register the sha1 of the new file + ansible.windows.win_stat: + path: "{{ remote_tmp_dir }}\\plane-utf16be-bom-chinese-characters.xml" + get_checksum: yes + register: sha1_checksum + +- name: verify the checksum + assert: + that: + - sha1_checksum.stat.checksum == 'de86f79b409383447cf4cf112b20af8ffffcfdbf' + +# features added ansible 2.8 +# count + +- name: count logger nodes in log4j.xml + win_xml: + path: "{{ remote_tmp_dir }}\\log4j.xml" + xpath: //logger + count: yes + register: logger_node_count + +- name: verify node count + assert: + that: + - logger_node_count.count == 5 + +# multiple attribute change +- name: ensure //logger/level value attributes are set to debug + win_xml: + path: "{{ remote_tmp_dir }}\\log4j.xml" + xpath: '//logger/level[@value="error"]' + type: attribute + attribute: value + fragment: debug + count: yes + register: logger_level_value_attrs + +- name: verify //logger/level value attributes + assert: + that: + - logger_level_value_attrs.count == 4 + - logger_level_value_attrs.changed == true + - logger_level_value_attrs.msg == 'attribute changed' + +- name: ensure //logger/level value attributes are set to debug (idempotency) + win_xml: + path: "{{ remote_tmp_dir }}\\log4j.xml" + xpath: '//logger/level[@value="error"]' + type: attribute + attribute: value + fragment: debug + count: yes + register: logger_level_value_attrs_again + +- name: verify //logger/level value attributes again (idempotency) + assert: + that: + - logger_level_value_attrs_again.count == 0 + - logger_level_value_attrs_again.changed == false + - logger_level_value_attrs_again.msg == 'The supplied xpath did not match any nodes. If this is unexpected, check your xpath is valid for the xml file at supplied path.' + +# multiple text nodes +- name: ensure test books.xml is present + ansible.windows.win_copy: + src: books.xml + dest: '{{ remote_tmp_dir }}\books.xml' + +- name: demonstrate multi text replace by replacing all title text elements + win_xml: + path: '{{ remote_tmp_dir }}\books.xml' + xpath: //works/title + type: text + fragment: _TITLE_TEXT_REMOVED_BY_WIN_XML_MODULE_ + count: yes + register: multi_text + +- name: verify multi text change + assert: + that: + - multi_text.changed == true + - multi_text.count == 5 + - multi_text.msg == 'text changed' + +- name: demonstrate multi text replace by replacing all title text elements again (idempotency) + win_xml: + path: '{{ remote_tmp_dir }}\books.xml' + xpath: //works/title + type: text + fragment: _TITLE_TEXT_REMOVED_BY_WIN_XML_MODULE_ + count: yes + register: multi_text_again + +- name: verify multi text again change (idempotency) + assert: + that: + - multi_text_again.changed == false + - multi_text_again.count == 5 + - multi_text_again.msg == 'not changed' + +# multiple element + +#- name: ensure a fresh test books.xml is present +# ansible.windows.win_copy: +# src: books.xml +# dest: '{{ remote_tmp_dir }}\books.xml' + +- name: demonstrate multi element should append new information element from fragment + win_xml: + path: '{{ remote_tmp_dir }}\books.xml' + xpath: //works/title + type: element + fragment: <information>This element added by ansible</information> + count: yes + register: multi_element + +- name: verify multi element + assert: + that: + - multi_element.changed == true + - multi_element.count == 5 + - multi_element.msg == 'element changed' + +- name: demonstrate multi element unchanged (idempotency) + win_xml: + path: '{{ remote_tmp_dir }}\books.xml' + xpath: //works/title + type: element + fragment: <information>This element added by ansible</information> + count: yes + register: multi_element_again + +- name: verify multi element again (idempotency) + assert: + that: + - multi_element_again.changed == false + - multi_element_again.count == 5 + - multi_element_again.msg == 'not changed' + +# multiple attributes on differing parent nodes + +- name: ensure all attribute lang=nl + win_xml: + path: '{{ remote_tmp_dir }}\books.xml' + xpath: //@lang + type: attribute + attribute: lang + fragment: nl + count: yes + register: multi_attr + +- name: verify multi attribute + assert: + that: + - multi_attr.changed == true + - multi_attr.count == 6 + - multi_attr.msg == 'attribute changed' + +- name: ensure all attribute lang=nl (idempotency) + win_xml: + path: '{{ remote_tmp_dir }}\books.xml' + xpath: //@lang + type: attribute + attribute: lang + fragment: nl + count: yes + register: multi_attr_again + +- name: verify multi attribute (idempotency) + assert: + that: + - multi_attr_again.changed == false + - multi_attr_again.count == 6 + - multi_attr_again.msg == 'not changed' diff --git a/ansible_collections/community/windows/tests/integration/targets/win_zip/aliases b/ansible_collections/community/windows/tests/integration/targets/win_zip/aliases new file mode 100644 index 000000000..b6fdaeae0 --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_zip/aliases @@ -0,0 +1 @@ +shippable/windows/group5
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_zip/defaults/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_zip/defaults/main.yml new file mode 100644 index 000000000..1b2520d5a --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_zip/defaults/main.yml @@ -0,0 +1,2 @@ +win_zip_name: .ÅÑŚÌβŁÈ [$!@^&test(;)] 👋 +win_zip_dir: '{{ remote_tmp_dir }}\win_zip {{ win_zip_name }}'
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/integration/targets/win_zip/meta/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_zip/meta/main.yml new file mode 100644 index 000000000..9f37e96cd --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_zip/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_remote_tmp_dir diff --git a/ansible_collections/community/windows/tests/integration/targets/win_zip/tasks/main.yml b/ansible_collections/community/windows/tests/integration/targets/win_zip/tasks/main.yml new file mode 100644 index 000000000..bdf98641a --- /dev/null +++ b/ansible_collections/community/windows/tests/integration/targets/win_zip/tasks/main.yml @@ -0,0 +1,165 @@ +--- +- set_fact: + zip_info: | + param ($Path) + + $ErrorActionPreference = 'Stop' + $Ansible.Changed = $false + + $utf8 = New-Object -TypeName Text.UTF8Encoding -ArgumentList $false + Add-Type -AssemblyName System.IO.Compression -ErrorAction Stop + Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction Stop + + $zip = $null + $fs = [IO.File]::OpenRead($Path) + try { + $zip = New-Object -TypeName IO.Compression.ZipArchive -ArgumentList $fs, 'Read', $false, $utf8 + + $zip.Entries | Select-Object -Propert FullName, Length + } + finally { + if ($zip) { $zip.Dispose() } + $fs.Dispose() + } + +- name: create testdir\src directory for CI + ansible.windows.win_file: + path: '{{ win_zip_dir }}\src\' + state: directory + +- name: create testdir\zipped directory for CI + ansible.windows.win_file: + path: '{{ win_zip_dir }}\zipped\' + state: directory + +- name: create test files for CI + ansible.builtin.win_copy: + content: | + This is a test file. + dest: '{{ win_zip_dir }}\src\{{ win_zip_name }}.txt' + +# Case01: Check file compression +- name: compress a file (check) + win_zip: + src: '{{ win_zip_dir }}\src\{{ win_zip_name }}.txt' + dest: '{{ win_zip_dir }}\zipped\test_file.zip' + register: zip_check + check_mode: yes + +- name: get result of compress zip (check) + ansible.windows.win_stat: + path: '{{ win_zip_dir }}\zipped\test_file.zip' + register: zip_actual_check + +- name: assert result of zip (check) + assert: + that: + - zip_check is changed + - not zip_actual_check.stat.exists + +- name: compress a file + win_zip: + src: '{{ win_zip_dir }}\src\{{ win_zip_name }}.txt' + dest: '{{ win_zip_dir }}\zipped\test_file.zip' + register: zip + +- name: get result of compress zip + ansible.windows.win_powershell: + script: '{{ zip_info }}' + parameters: + Path: '{{ win_zip_dir }}\zipped\test_file.zip' + register: zip_actual + +- name: assert result of zip + assert: + that: + - zip is changed + - zip_actual.output | length == 1 + - zip_actual.output[0]['FullName'] == win_zip_name + '.txt' + - zip_actual.output[0]['Length'] == 21 + +# Case02: Check directory compression +- name: compress a directory (check) + win_zip: + src: '{{ win_zip_dir }}\src\' + dest: '{{ win_zip_dir }}\test_dir.zip' + register: zip_check + check_mode: yes + +- name: get result of compress zip (check) + ansible.windows.win_stat: + path: '{{ win_zip_dir }}\test_dir.zip' + register: zip_actual_check + +- name: assert result of zip (check) + assert: + that: + - zip_check is changed + - not zip_actual_check.stat.exists + +- name: compress a directory + win_zip: + src: '{{ win_zip_dir }}\src\' + dest: '{{ win_zip_dir }}\test_dir.zip' + register: zip + +- name: get result of compress zip + ansible.windows.win_powershell: + script: '{{ zip_info }}' + parameters: + Path: '{{ win_zip_dir }}\test_dir.zip' + register: zip_actual + +- name: assert result of zip + assert: + that: + - zip is changed + - zip_actual.output | length == 1 + # Should contain the original base directory + - zip_actual.output[0]['FullName'] == "src/" + win_zip_name + '.txt' + - zip_actual.output[0]['Length'] == 21 + +- name: compress a directories contents + win_zip: + src: '{{ win_zip_dir }}\src\*' + dest: '{{ win_zip_dir }}\test_dir_content.zip' + register: zip + +- name: get result of compress zip + ansible.windows.win_powershell: + script: '{{ zip_info }}' + parameters: + Path: '{{ win_zip_dir }}\test_dir_content.zip' + register: zip_actual + +- name: assert result of zip + assert: + that: + - zip is changed + - zip_actual.output | length == 1 + # Should not contain the original base directory + - zip_actual.output[0]['FullName'] == win_zip_name + '.txt' + - zip_actual.output[0]['Length'] == 21 + +- name: compress a file with existing dest (check) + win_zip: + src: '{{ win_zip_dir }}\src\{{ win_zip_name }}.txt' + dest: '{{ win_zip_dir }}\zipped\test_file.zip' + register: zip_check + check_mode: yes + +- name: assert result of zip (check) + assert: + that: + - not zip_check is changed + +- name: compress a file to existing dest + win_zip: + src: '{{ win_zip_dir }}\src\{{ win_zip_name }}.txt' + dest: '{{ win_zip_dir }}\zipped\test_file.zip' + register: zip + +- name: assert result of zip + assert: + that: + - not zip is changed diff --git a/ansible_collections/community/windows/tests/requirements.yml b/ansible_collections/community/windows/tests/requirements.yml new file mode 100644 index 000000000..4f4960ebd --- /dev/null +++ b/ansible_collections/community/windows/tests/requirements.yml @@ -0,0 +1,6 @@ +collections: +- name: ansible.windows +- name: chocolatey.chocolatey + # Chocolatey 1.3.0 broke compatibiltiy wtih WinPS 3 and 4 so we are stuck with 1.2.0 + # https://github.com/chocolatey/chocolatey-ansible/issues/96 + version: <1.3.0
\ No newline at end of file diff --git a/ansible_collections/community/windows/tests/sanity/ignore-2.12.txt b/ansible_collections/community/windows/tests/sanity/ignore-2.12.txt new file mode 100644 index 000000000..84d7bb121 --- /dev/null +++ b/ansible_collections/community/windows/tests/sanity/ignore-2.12.txt @@ -0,0 +1,13 @@ +plugins/modules/win_audit_rule.ps1 pslint:PSCustomUseLiteralPath +plugins/modules/win_rabbitmq_plugin.ps1 pslint:PSAvoidUsingInvokeExpression +plugins/modules/win_region.ps1 pslint:PSAvoidUsingEmptyCatchBlock # Keep +plugins/modules/win_regmerge.ps1 pslint:PSCustomUseLiteralPath +plugins/modules/win_robocopy.ps1 pslint:PSCustomUseLiteralPath +tests/integration/targets/win_audit_rule/library/test_get_audit_rule.ps1 pslint:PSCustomUseLiteralPath +tests/integration/targets/win_lineinfile/files/expectations/23_utf8_bom.txt shebang +tests/integration/targets/win_lineinfile/files/expectations/24_utf8_bom_line_added.txt shebang +tests/integration/targets/win_lineinfile/files/expectations/30_linebreaks_checksum_bad.txt line-endings +tests/integration/targets/win_psmodule/files/module/template.psd1 pslint!skip +tests/integration/targets/win_psmodule/files/module/template.psm1 pslint!skip +tests/integration/targets/win_psmodule/files/setup_modules.ps1 pslint:PSCustomUseLiteralPath +tests/integration/targets/win_regmerge/templates/win_line_ending.j2 line-endings diff --git a/ansible_collections/community/windows/tests/sanity/ignore-2.13.txt b/ansible_collections/community/windows/tests/sanity/ignore-2.13.txt new file mode 100644 index 000000000..84d7bb121 --- /dev/null +++ b/ansible_collections/community/windows/tests/sanity/ignore-2.13.txt @@ -0,0 +1,13 @@ +plugins/modules/win_audit_rule.ps1 pslint:PSCustomUseLiteralPath +plugins/modules/win_rabbitmq_plugin.ps1 pslint:PSAvoidUsingInvokeExpression +plugins/modules/win_region.ps1 pslint:PSAvoidUsingEmptyCatchBlock # Keep +plugins/modules/win_regmerge.ps1 pslint:PSCustomUseLiteralPath +plugins/modules/win_robocopy.ps1 pslint:PSCustomUseLiteralPath +tests/integration/targets/win_audit_rule/library/test_get_audit_rule.ps1 pslint:PSCustomUseLiteralPath +tests/integration/targets/win_lineinfile/files/expectations/23_utf8_bom.txt shebang +tests/integration/targets/win_lineinfile/files/expectations/24_utf8_bom_line_added.txt shebang +tests/integration/targets/win_lineinfile/files/expectations/30_linebreaks_checksum_bad.txt line-endings +tests/integration/targets/win_psmodule/files/module/template.psd1 pslint!skip +tests/integration/targets/win_psmodule/files/module/template.psm1 pslint!skip +tests/integration/targets/win_psmodule/files/setup_modules.ps1 pslint:PSCustomUseLiteralPath +tests/integration/targets/win_regmerge/templates/win_line_ending.j2 line-endings diff --git a/ansible_collections/community/windows/tests/sanity/ignore-2.14.txt b/ansible_collections/community/windows/tests/sanity/ignore-2.14.txt new file mode 100644 index 000000000..84d7bb121 --- /dev/null +++ b/ansible_collections/community/windows/tests/sanity/ignore-2.14.txt @@ -0,0 +1,13 @@ +plugins/modules/win_audit_rule.ps1 pslint:PSCustomUseLiteralPath +plugins/modules/win_rabbitmq_plugin.ps1 pslint:PSAvoidUsingInvokeExpression +plugins/modules/win_region.ps1 pslint:PSAvoidUsingEmptyCatchBlock # Keep +plugins/modules/win_regmerge.ps1 pslint:PSCustomUseLiteralPath +plugins/modules/win_robocopy.ps1 pslint:PSCustomUseLiteralPath +tests/integration/targets/win_audit_rule/library/test_get_audit_rule.ps1 pslint:PSCustomUseLiteralPath +tests/integration/targets/win_lineinfile/files/expectations/23_utf8_bom.txt shebang +tests/integration/targets/win_lineinfile/files/expectations/24_utf8_bom_line_added.txt shebang +tests/integration/targets/win_lineinfile/files/expectations/30_linebreaks_checksum_bad.txt line-endings +tests/integration/targets/win_psmodule/files/module/template.psd1 pslint!skip +tests/integration/targets/win_psmodule/files/module/template.psm1 pslint!skip +tests/integration/targets/win_psmodule/files/setup_modules.ps1 pslint:PSCustomUseLiteralPath +tests/integration/targets/win_regmerge/templates/win_line_ending.j2 line-endings diff --git a/ansible_collections/community/windows/tests/sanity/ignore-2.15.txt b/ansible_collections/community/windows/tests/sanity/ignore-2.15.txt new file mode 100644 index 000000000..84d7bb121 --- /dev/null +++ b/ansible_collections/community/windows/tests/sanity/ignore-2.15.txt @@ -0,0 +1,13 @@ +plugins/modules/win_audit_rule.ps1 pslint:PSCustomUseLiteralPath +plugins/modules/win_rabbitmq_plugin.ps1 pslint:PSAvoidUsingInvokeExpression +plugins/modules/win_region.ps1 pslint:PSAvoidUsingEmptyCatchBlock # Keep +plugins/modules/win_regmerge.ps1 pslint:PSCustomUseLiteralPath +plugins/modules/win_robocopy.ps1 pslint:PSCustomUseLiteralPath +tests/integration/targets/win_audit_rule/library/test_get_audit_rule.ps1 pslint:PSCustomUseLiteralPath +tests/integration/targets/win_lineinfile/files/expectations/23_utf8_bom.txt shebang +tests/integration/targets/win_lineinfile/files/expectations/24_utf8_bom_line_added.txt shebang +tests/integration/targets/win_lineinfile/files/expectations/30_linebreaks_checksum_bad.txt line-endings +tests/integration/targets/win_psmodule/files/module/template.psd1 pslint!skip +tests/integration/targets/win_psmodule/files/module/template.psm1 pslint!skip +tests/integration/targets/win_psmodule/files/setup_modules.ps1 pslint:PSCustomUseLiteralPath +tests/integration/targets/win_regmerge/templates/win_line_ending.j2 line-endings diff --git a/ansible_collections/community/windows/tests/sanity/ignore-2.16.txt b/ansible_collections/community/windows/tests/sanity/ignore-2.16.txt new file mode 100644 index 000000000..84d7bb121 --- /dev/null +++ b/ansible_collections/community/windows/tests/sanity/ignore-2.16.txt @@ -0,0 +1,13 @@ +plugins/modules/win_audit_rule.ps1 pslint:PSCustomUseLiteralPath +plugins/modules/win_rabbitmq_plugin.ps1 pslint:PSAvoidUsingInvokeExpression +plugins/modules/win_region.ps1 pslint:PSAvoidUsingEmptyCatchBlock # Keep +plugins/modules/win_regmerge.ps1 pslint:PSCustomUseLiteralPath +plugins/modules/win_robocopy.ps1 pslint:PSCustomUseLiteralPath +tests/integration/targets/win_audit_rule/library/test_get_audit_rule.ps1 pslint:PSCustomUseLiteralPath +tests/integration/targets/win_lineinfile/files/expectations/23_utf8_bom.txt shebang +tests/integration/targets/win_lineinfile/files/expectations/24_utf8_bom_line_added.txt shebang +tests/integration/targets/win_lineinfile/files/expectations/30_linebreaks_checksum_bad.txt line-endings +tests/integration/targets/win_psmodule/files/module/template.psd1 pslint!skip +tests/integration/targets/win_psmodule/files/module/template.psm1 pslint!skip +tests/integration/targets/win_psmodule/files/setup_modules.ps1 pslint:PSCustomUseLiteralPath +tests/integration/targets/win_regmerge/templates/win_line_ending.j2 line-endings diff --git a/ansible_collections/community/windows/tests/unit/__init__.py b/ansible_collections/community/windows/tests/unit/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/community/windows/tests/unit/__init__.py diff --git a/ansible_collections/community/windows/tests/unit/compat/__init__.py b/ansible_collections/community/windows/tests/unit/compat/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/community/windows/tests/unit/compat/__init__.py diff --git a/ansible_collections/community/windows/tests/unit/compat/mock.py b/ansible_collections/community/windows/tests/unit/compat/mock.py new file mode 100644 index 000000000..3dcd2687f --- /dev/null +++ b/ansible_collections/community/windows/tests/unit/compat/mock.py @@ -0,0 +1,42 @@ +# (c) 2014, Toshio Kuratomi <tkuratomi@ansible.com> +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +''' +Compat module for Python3.x's unittest.mock module +''' + +# Python 2.7 + +# Note: Could use the pypi mock library on python3.x as well as python2.x. It +# is the same as the python3 stdlib mock library + +try: + # Allow wildcard import because we really do want to import all of mock's + # symbols into this compat shim + # pylint: disable=wildcard-import,unused-wildcard-import + from unittest.mock import * +except ImportError: + # Python 2 + # pylint: disable=wildcard-import,unused-wildcard-import + try: + from mock import * + except ImportError: + print('You need the mock library installed on python2.x to run tests') diff --git a/ansible_collections/community/windows/tests/unit/conftest.py b/ansible_collections/community/windows/tests/unit/conftest.py new file mode 100644 index 000000000..e3f2ec4a0 --- /dev/null +++ b/ansible_collections/community/windows/tests/unit/conftest.py @@ -0,0 +1,43 @@ +"""Enable unit testing of Ansible collections. PYTEST_DONT_REWRITE""" +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import os.path + +from ansible.utils.collection_loader._collection_finder import _AnsibleCollectionFinder + + +ANSIBLE_COLLECTIONS_PATH = os.path.abspath(os.path.join(__file__, '..', '..', '..', '..', '..')) + + +# this monkeypatch to _pytest.pathlib.resolve_package_path fixes PEP420 resolution for collections in pytest >= 6.0.0 +def collection_resolve_package_path(path): + """Configure the Python package path so that pytest can find our collections.""" + for parent in path.parents: + if str(parent) == ANSIBLE_COLLECTIONS_PATH: + return parent + + raise Exception('File "%s" not found in collection path "%s".' % (path, ANSIBLE_COLLECTIONS_PATH)) + + +def pytest_configure(): + """Configure this pytest plugin.""" + + try: + if pytest_configure.executed: + return + except AttributeError: + pytest_configure.executed = True + + # allow unit tests to import code from collections + + # noinspection PyProtectedMember + _AnsibleCollectionFinder(paths=[os.path.dirname(ANSIBLE_COLLECTIONS_PATH)])._install() # pylint: disable=protected-access + + # noinspection PyProtectedMember + from _pytest import pathlib as pytest_pathlib + pytest_pathlib.resolve_package_path = collection_resolve_package_path + + +pytest_configure() diff --git a/ansible_collections/community/windows/tests/unit/mock/__init__.py b/ansible_collections/community/windows/tests/unit/mock/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/community/windows/tests/unit/mock/__init__.py diff --git a/ansible_collections/community/windows/tests/unit/mock/loader.py b/ansible_collections/community/windows/tests/unit/mock/loader.py new file mode 100644 index 000000000..e5dff78c1 --- /dev/null +++ b/ansible_collections/community/windows/tests/unit/mock/loader.py @@ -0,0 +1,116 @@ +# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com> +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os + +from ansible.errors import AnsibleParserError +from ansible.parsing.dataloader import DataLoader +from ansible.module_utils._text import to_bytes, to_text + + +class DictDataLoader(DataLoader): + + def __init__(self, file_mapping=None): + file_mapping = {} if file_mapping is None else file_mapping + assert type(file_mapping) == dict + + super(DictDataLoader, self).__init__() + + self._file_mapping = file_mapping + self._build_known_directories() + self._vault_secrets = None + + def load_from_file(self, path, cache=True, unsafe=False): + path = to_text(path) + if path in self._file_mapping: + return self.load(self._file_mapping[path], path) + return None + + # TODO: the real _get_file_contents returns a bytestring, so we actually convert the + # unicode/text it's created with to utf-8 + def _get_file_contents(self, file_name): + path = to_text(file_name) + if path in self._file_mapping: + return (to_bytes(self._file_mapping[path]), False) + else: + raise AnsibleParserError("file not found: %s" % path) + + def path_exists(self, path): + path = to_text(path) + return path in self._file_mapping or path in self._known_directories + + def is_file(self, path): + path = to_text(path) + return path in self._file_mapping + + def is_directory(self, path): + path = to_text(path) + return path in self._known_directories + + def list_directory(self, path): + ret = [] + path = to_text(path) + for x in (list(self._file_mapping.keys()) + self._known_directories): + if x.startswith(path): + if os.path.dirname(x) == path: + ret.append(os.path.basename(x)) + return ret + + def is_executable(self, path): + # FIXME: figure out a way to make paths return true for this + return False + + def _add_known_directory(self, directory): + if directory not in self._known_directories: + self._known_directories.append(directory) + + def _build_known_directories(self): + self._known_directories = [] + for path in self._file_mapping: + dirname = os.path.dirname(path) + while dirname not in ('/', ''): + self._add_known_directory(dirname) + dirname = os.path.dirname(dirname) + + def push(self, path, content): + rebuild_dirs = False + if path not in self._file_mapping: + rebuild_dirs = True + + self._file_mapping[path] = content + + if rebuild_dirs: + self._build_known_directories() + + def pop(self, path): + if path in self._file_mapping: + del self._file_mapping[path] + self._build_known_directories() + + def clear(self): + self._file_mapping = dict() + self._known_directories = [] + + def get_basedir(self): + return os.getcwd() + + def set_vault_secrets(self, vault_secrets): + self._vault_secrets = vault_secrets diff --git a/ansible_collections/community/windows/tests/unit/mock/path.py b/ansible_collections/community/windows/tests/unit/mock/path.py new file mode 100644 index 000000000..54858b13d --- /dev/null +++ b/ansible_collections/community/windows/tests/unit/mock/path.py @@ -0,0 +1,8 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible_collections.ansible.windows.tests.unit.compat.mock import MagicMock +from ansible.utils.path import unfrackpath + + +mock_unfrackpath_noop = MagicMock(spec_set=unfrackpath, side_effect=lambda x, *args, **kwargs: x) diff --git a/ansible_collections/community/windows/tests/unit/mock/procenv.py b/ansible_collections/community/windows/tests/unit/mock/procenv.py new file mode 100644 index 000000000..3cb1b5b2f --- /dev/null +++ b/ansible_collections/community/windows/tests/unit/mock/procenv.py @@ -0,0 +1,90 @@ +# (c) 2016, Matt Davis <mdavis@ansible.com> +# (c) 2016, Toshio Kuratomi <tkuratomi@ansible.com> +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import sys +import json + +from contextlib import contextmanager +from io import BytesIO, StringIO +from ansible_collections.community.windows.tests.unit.compat import unittest +from ansible.module_utils.six import PY3 +from ansible.module_utils._text import to_bytes + + +@contextmanager +def swap_stdin_and_argv(stdin_data='', argv_data=tuple()): + """ + context manager that temporarily masks the test runner's values for stdin and argv + """ + real_stdin = sys.stdin + real_argv = sys.argv + + if PY3: + fake_stream = StringIO(stdin_data) + fake_stream.buffer = BytesIO(to_bytes(stdin_data)) + else: + fake_stream = BytesIO(to_bytes(stdin_data)) + + try: + sys.stdin = fake_stream + sys.argv = argv_data + + yield + finally: + sys.stdin = real_stdin + sys.argv = real_argv + + +@contextmanager +def swap_stdout(): + """ + context manager that temporarily replaces stdout for tests that need to verify output + """ + old_stdout = sys.stdout + + if PY3: + fake_stream = StringIO() + else: + fake_stream = BytesIO() + + try: + sys.stdout = fake_stream + + yield fake_stream + finally: + sys.stdout = old_stdout + + +class ModuleTestCase(unittest.TestCase): + def setUp(self, module_args=None): + if module_args is None: + module_args = {'_ansible_remote_tmp': '/tmp', '_ansible_keep_remote_files': False} + + args = json.dumps(dict(ANSIBLE_MODULE_ARGS=module_args)) + + # unittest doesn't have a clean place to use a context manager, so we have to enter/exit manually + self.stdin_swap = swap_stdin_and_argv(stdin_data=args) + self.stdin_swap.__enter__() + + def tearDown(self): + # unittest doesn't have a clean place to use a context manager, so we have to enter/exit manually + self.stdin_swap.__exit__(None, None, None) diff --git a/ansible_collections/community/windows/tests/unit/mock/vault_helper.py b/ansible_collections/community/windows/tests/unit/mock/vault_helper.py new file mode 100644 index 000000000..dcce9c784 --- /dev/null +++ b/ansible_collections/community/windows/tests/unit/mock/vault_helper.py @@ -0,0 +1,39 @@ +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.module_utils._text import to_bytes + +from ansible.parsing.vault import VaultSecret + + +class TextVaultSecret(VaultSecret): + '''A secret piece of text. ie, a password. Tracks text encoding. + + The text encoding of the text may not be the default text encoding so + we keep track of the encoding so we encode it to the same bytes.''' + + def __init__(self, text, encoding=None, errors=None, _bytes=None): + super(TextVaultSecret, self).__init__() + self.text = text + self.encoding = encoding or 'utf-8' + self._bytes = _bytes + self.errors = errors or 'strict' + + @property + def bytes(self): + '''The text encoded with encoding, unless we specifically set _bytes.''' + return self._bytes or to_bytes(self.text, encoding=self.encoding, errors=self.errors) diff --git a/ansible_collections/community/windows/tests/unit/mock/yaml_helper.py b/ansible_collections/community/windows/tests/unit/mock/yaml_helper.py new file mode 100644 index 000000000..1ef172159 --- /dev/null +++ b/ansible_collections/community/windows/tests/unit/mock/yaml_helper.py @@ -0,0 +1,124 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import io +import yaml + +from ansible.module_utils.six import PY3 +from ansible.parsing.yaml.loader import AnsibleLoader +from ansible.parsing.yaml.dumper import AnsibleDumper + + +class YamlTestUtils(object): + """Mixin class to combine with a unittest.TestCase subclass.""" + def _loader(self, stream): + """Vault related tests will want to override this. + + Vault cases should setup a AnsibleLoader that has the vault password.""" + return AnsibleLoader(stream) + + def _dump_stream(self, obj, stream, dumper=None): + """Dump to a py2-unicode or py3-string stream.""" + if PY3: + return yaml.dump(obj, stream, Dumper=dumper) + else: + return yaml.dump(obj, stream, Dumper=dumper, encoding=None) + + def _dump_string(self, obj, dumper=None): + """Dump to a py2-unicode or py3-string""" + if PY3: + return yaml.dump(obj, Dumper=dumper) + else: + return yaml.dump(obj, Dumper=dumper, encoding=None) + + def _dump_load_cycle(self, obj): + # Each pass though a dump or load revs the 'generation' + # obj to yaml string + string_from_object_dump = self._dump_string(obj, dumper=AnsibleDumper) + + # wrap a stream/file like StringIO around that yaml + stream_from_object_dump = io.StringIO(string_from_object_dump) + loader = self._loader(stream_from_object_dump) + # load the yaml stream to create a new instance of the object (gen 2) + obj_2 = loader.get_data() + + # dump the gen 2 objects directory to strings + string_from_object_dump_2 = self._dump_string(obj_2, + dumper=AnsibleDumper) + + # The gen 1 and gen 2 yaml strings + self.assertEqual(string_from_object_dump, string_from_object_dump_2) + # the gen 1 (orig) and gen 2 py object + self.assertEqual(obj, obj_2) + + # again! gen 3... load strings into py objects + stream_3 = io.StringIO(string_from_object_dump_2) + loader_3 = self._loader(stream_3) + obj_3 = loader_3.get_data() + + string_from_object_dump_3 = self._dump_string(obj_3, dumper=AnsibleDumper) + + self.assertEqual(obj, obj_3) + # should be transitive, but... + self.assertEqual(obj_2, obj_3) + self.assertEqual(string_from_object_dump, string_from_object_dump_3) + + def _old_dump_load_cycle(self, obj): + '''Dump the passed in object to yaml, load it back up, dump again, compare.''' + stream = io.StringIO() + + yaml_string = self._dump_string(obj, dumper=AnsibleDumper) + self._dump_stream(obj, stream, dumper=AnsibleDumper) + + yaml_string_from_stream = stream.getvalue() + + # reset stream + stream.seek(0) + + loader = self._loader(stream) + # loader = AnsibleLoader(stream, vault_password=self.vault_password) + obj_from_stream = loader.get_data() + + stream_from_string = io.StringIO(yaml_string) + loader2 = self._loader(stream_from_string) + # loader2 = AnsibleLoader(stream_from_string, vault_password=self.vault_password) + obj_from_string = loader2.get_data() + + stream_obj_from_stream = io.StringIO() + stream_obj_from_string = io.StringIO() + + if PY3: + yaml.dump(obj_from_stream, stream_obj_from_stream, Dumper=AnsibleDumper) + yaml.dump(obj_from_stream, stream_obj_from_string, Dumper=AnsibleDumper) + else: + yaml.dump(obj_from_stream, stream_obj_from_stream, Dumper=AnsibleDumper, encoding=None) + yaml.dump(obj_from_stream, stream_obj_from_string, Dumper=AnsibleDumper, encoding=None) + + yaml_string_stream_obj_from_stream = stream_obj_from_stream.getvalue() + yaml_string_stream_obj_from_string = stream_obj_from_string.getvalue() + + stream_obj_from_stream.seek(0) + stream_obj_from_string.seek(0) + + if PY3: + yaml_string_obj_from_stream = yaml.dump(obj_from_stream, Dumper=AnsibleDumper) + yaml_string_obj_from_string = yaml.dump(obj_from_string, Dumper=AnsibleDumper) + else: + yaml_string_obj_from_stream = yaml.dump(obj_from_stream, Dumper=AnsibleDumper, encoding=None) + yaml_string_obj_from_string = yaml.dump(obj_from_string, Dumper=AnsibleDumper, encoding=None) + + assert yaml_string == yaml_string_obj_from_stream + assert yaml_string == yaml_string_obj_from_stream == yaml_string_obj_from_string + assert (yaml_string == yaml_string_obj_from_stream == yaml_string_obj_from_string == yaml_string_stream_obj_from_stream == + yaml_string_stream_obj_from_string) + assert obj == obj_from_stream + assert obj == obj_from_string + assert obj == yaml_string_obj_from_stream + assert obj == yaml_string_obj_from_string + assert obj == obj_from_stream == obj_from_string == yaml_string_obj_from_stream == yaml_string_obj_from_string + return {'obj': obj, + 'yaml_string': yaml_string, + 'yaml_string_from_stream': yaml_string_from_stream, + 'obj_from_stream': obj_from_stream, + 'obj_from_string': obj_from_string, + 'yaml_string_obj_from_string': yaml_string_obj_from_string} diff --git a/ansible_collections/community/windows/tests/unit/modules/__init__.py b/ansible_collections/community/windows/tests/unit/modules/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/community/windows/tests/unit/modules/__init__.py diff --git a/ansible_collections/community/windows/tests/unit/modules/utils.py b/ansible_collections/community/windows/tests/unit/modules/utils.py new file mode 100644 index 000000000..bc627df64 --- /dev/null +++ b/ansible_collections/community/windows/tests/unit/modules/utils.py @@ -0,0 +1,50 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import json + +from ansible_collections.community.windows.tests.unit.compat import unittest +from ansible_collections.community.windows.tests.unit.compat.mock import patch +from ansible.module_utils import basic +from ansible.module_utils._text import to_bytes + + +def set_module_args(args): + if '_ansible_remote_tmp' not in args: + args['_ansible_remote_tmp'] = '/tmp' + if '_ansible_keep_remote_files' not in args: + args['_ansible_keep_remote_files'] = False + + args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) + basic._ANSIBLE_ARGS = to_bytes(args) + + +class AnsibleExitJson(Exception): + pass + + +class AnsibleFailJson(Exception): + pass + + +def exit_json(*args, **kwargs): + if 'changed' not in kwargs: + kwargs['changed'] = False + raise AnsibleExitJson(kwargs) + + +def fail_json(*args, **kwargs): + kwargs['failed'] = True + raise AnsibleFailJson(kwargs) + + +class ModuleTestCase(unittest.TestCase): + + def setUp(self): + self.mock_module = patch.multiple(basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json) + self.mock_module.start() + self.mock_sleep = patch('time.sleep') + self.mock_sleep.start() + set_module_args({}) + self.addCleanup(self.mock_module.stop) + self.addCleanup(self.mock_sleep.stop) diff --git a/ansible_collections/community/windows/tests/unit/plugins/__init__.py b/ansible_collections/community/windows/tests/unit/plugins/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/community/windows/tests/unit/plugins/__init__.py diff --git a/ansible_collections/community/windows/tests/unit/plugins/lookup/__init__.py b/ansible_collections/community/windows/tests/unit/plugins/lookup/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/community/windows/tests/unit/plugins/lookup/__init__.py diff --git a/ansible_collections/community/windows/tests/unit/plugins/lookup/fixtures/avi.json b/ansible_collections/community/windows/tests/unit/plugins/lookup/fixtures/avi.json new file mode 100644 index 000000000..ae89ca689 --- /dev/null +++ b/ansible_collections/community/windows/tests/unit/plugins/lookup/fixtures/avi.json @@ -0,0 +1,104 @@ +{ + "mock_single_obj": { + "_last_modified": "", + "cloud_ref": "https://192.0.2.132/api/cloud/cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "dhcp_enabled": true, + "exclude_discovered_subnets": false, + "name": "PG-123", + "synced_from_se": true, + "tenant_ref": "https://192.0.2.132/api/tenant/admin", + "url": "https://192.0.2.132/api/network/dvportgroup-2084-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "uuid": "dvportgroup-2084-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vcenter_dvs": true, + "vimgrnw_ref": "https://192.0.2.132/api/vimgrnwruntime/dvportgroup-2084-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vrf_context_ref": "https://192.0.2.132/api/vrfcontext/vrfcontext-31f1b55f-319c-44eb-862f-69d79ffdf295" + }, + "mock_multiple_obj": { + "results": [ + { + "_last_modified": "", + "cloud_ref": "https://192.0.2.132/api/cloud/cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "dhcp_enabled": true, + "exclude_discovered_subnets": false, + "name": "J-PG-0682", + "synced_from_se": true, + "tenant_ref": "https://192.0.2.132/api/tenant/admin", + "url": "https://192.0.2.132/api/network/dvportgroup-2084-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "uuid": "dvportgroup-2084-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vcenter_dvs": true, + "vimgrnw_ref": "https://192.0.2.132/api/vimgrnwruntime/dvportgroup-2084-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vrf_context_ref": "https://192.0.2.132/api/vrfcontext/vrfcontext-31f1b55f-319c-44eb-862f-69d79ffdf295" + }, + { + "_last_modified": "", + "cloud_ref": "https://192.0.2.132/api/cloud/cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "dhcp_enabled": true, + "exclude_discovered_subnets": false, + "name": "J-PG-0231", + "synced_from_se": true, + "tenant_ref": "https://192.0.2.132/api/tenant/admin", + "url": "https://192.0.2.132/api/network/dvportgroup-1627-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "uuid": "dvportgroup-1627-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vcenter_dvs": true, + "vimgrnw_ref": "https://192.0.2.132/api/vimgrnwruntime/dvportgroup-1627-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vrf_context_ref": "https://192.0.2.132/api/vrfcontext/vrfcontext-31f1b55f-319c-44eb-862f-69d79ffdf295" + }, + { + "_last_modified": "", + "cloud_ref": "https://192.0.2.132/api/cloud/cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "dhcp_enabled": true, + "exclude_discovered_subnets": false, + "name": "J-PG-0535", + "synced_from_se": true, + "tenant_ref": "https://192.0.2.132/api/tenant/admin", + "url": "https://192.0.2.132/api/network/dvportgroup-1934-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "uuid": "dvportgroup-1934-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vcenter_dvs": true, + "vimgrnw_ref": "https://192.0.2.132/api/vimgrnwruntime/dvportgroup-1934-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vrf_context_ref": "https://192.0.2.132/api/vrfcontext/vrfcontext-31f1b55f-319c-44eb-862f-69d79ffdf295" + }, + { + "_last_modified": "", + "cloud_ref": "https://192.0.2.132/api/cloud/cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "dhcp_enabled": true, + "exclude_discovered_subnets": false, + "name": "J-PG-0094", + "synced_from_se": true, + "tenant_ref": "https://192.0.2.132/api/tenant/admin", + "url": "https://192.0.2.132/api/network/dvportgroup-1458-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "uuid": "dvportgroup-1458-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vcenter_dvs": true, + "vimgrnw_ref": "https://192.0.2.132/api/vimgrnwruntime/dvportgroup-1458-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vrf_context_ref": "https://192.0.2.132/api/vrfcontext/vrfcontext-31f1b55f-319c-44eb-862f-69d79ffdf295" + }, + { + "_last_modified": "", + "cloud_ref": "https://192.0.2.132/api/cloud/cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "dhcp_enabled": true, + "exclude_discovered_subnets": false, + "name": "J-PG-0437", + "synced_from_se": true, + "tenant_ref": "https://192.0.2.132/api/tenant/admin", + "url": "https://192.0.2.132/api/network/dvportgroup-1836-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "uuid": "dvportgroup-1836-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vcenter_dvs": true, + "vimgrnw_ref": "https://192.0.2.132/api/vimgrnwruntime/dvportgroup-1836-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vrf_context_ref": "https://192.0.2.132/api/vrfcontext/vrfcontext-31f1b55f-319c-44eb-862f-69d79ffdf295" + }, + { + "_last_modified": "", + "cloud_ref": "https://192.0.2.132/api/cloud/cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "dhcp_enabled": true, + "exclude_discovered_subnets": false, + "name": "J-PG-0673", + "synced_from_se": true, + "tenant_ref": "https://192.0.2.132/api/tenant/admin", + "url": "https://192.0.2.132/api/network/dvportgroup-2075-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "uuid": "dvportgroup-2075-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vcenter_dvs": true, + "vimgrnw_ref": "https://192.0.2.132/api/vimgrnwruntime/dvportgroup-2075-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", + "vrf_context_ref": "https://192.0.2.132/api/vrfcontext/vrfcontext-31f1b55f-319c-44eb-862f-69d79ffdf295" + } + ] + } +} diff --git a/ansible_collections/community/windows/tests/unit/plugins/lookup/test_laps_password.py b/ansible_collections/community/windows/tests/unit/plugins/lookup/test_laps_password.py new file mode 100644 index 000000000..29e2b938a --- /dev/null +++ b/ansible_collections/community/windows/tests/unit/plugins/lookup/test_laps_password.py @@ -0,0 +1,511 @@ +# -*- coding: utf-8 -*- +# (c) 2019, Jordan Borean <jborean@redhat.com> +# 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 os +import platform +import pytest +import sys + +from ansible_collections.community.windows.tests.unit.compat.mock import MagicMock + +from ansible.errors import AnsibleLookupError +from ansible.plugins.loader import lookup_loader + + +class FakeLDAPError(Exception): + pass + + +class FakeLDAPAuthUnknownError(Exception): + pass + + +class FakeLdap(object): + SASL_AVAIL = 1 + TLS_AVAIL = 1 + + SCOPE_SUBTREE = 2 + + OPT_PROTOCOL_VERSION = 17 + OPT_REFERRALS = 8 + + OPT_X_TLS_NEVER = 0 + OPT_X_TLS_DEMAND = 2 + OPT_X_TLS_ALLOW = 3 + OPT_X_TLS_TRY = 4 + + OPT_X_TLS_CACERTFILE = 24578 + OPT_X_TLS_REQUIRE_CERT = 24582 + + LDAPError = FakeLDAPError + AUTH_UNKNOWN = FakeLDAPAuthUnknownError + + @staticmethod + def initialize(uri, bytes_mode=None, **kwargs): + return MagicMock() + + @staticmethod + def set_option(option, invalue): + pass + + +class FakeLdapUrl(object): + + def __init__(self, ldapUrl=None, urlscheme='ldap', hostport='', **kwargs): + url = ldapUrl if ldapUrl else "%s://%s" % (urlscheme, hostport) + self.urlscheme = url.split('://', 2)[0].lower() + self._url = url + + def initializeUrl(self): + return self._url + + +def fake_is_ldap_url(s): + s_lower = s.lower() + return s_lower.startswith("ldap://") or s_lower.startswith("ldaps://") or s_lower.startswith("ldapi://") + + +@pytest.fixture(autouse=True) +def laps_password(): + """Imports and the laps_password lookup with a mocks laps module for testing""" + + # Build the fake ldap and ldapurl Python modules + fake_ldap_obj = FakeLdap() + fake_ldap_url_obj = MagicMock() + fake_ldap_url_obj.isLDAPUrl.side_effect = fake_is_ldap_url + fake_ldap_url_obj.LDAPUrl.side_effect = FakeLdapUrl + + # Take a snapshot of sys.modules before we manipulate it + orig_modules = sys.modules.copy() + try: + sys.modules["ldap"] = fake_ldap_obj + sys.modules["ldapurl"] = fake_ldap_url_obj + + from ansible_collections.community.windows.plugins.lookup import laps_password as lookup_obj + + # ensure the changes to these globals aren't persisted after each test + orig_has_ldap = lookup_obj.HAS_LDAP + orig_ldap_imp_err = lookup_obj.LDAP_IMP_ERR + + yield lookup_obj + + lookup_obj.HAS_LDAP = orig_has_ldap + lookup_obj.LDAP_IMP_ERR = orig_ldap_imp_err + finally: + # Restore sys.modules back to our pre-shenanigans + sys.modules = orig_modules + + +def test_missing_ldap(laps_password): + laps_password.HAS_LDAP = False + laps_password.LDAP_IMP_ERR = "no import for you!" + + with pytest.raises(AnsibleLookupError) as err: + lookup_loader.get('community.windows.laps_password').run(["host"], domain="test") + + assert str(err.value).startswith( + "Failed to import the required Python library (python-ldap) on %s's Python %s. See " + "https://pypi.org/project/python-ldap/ for more info. Please " + % (platform.node(), sys.executable) + ) + assert str(err.value).endswith(". Import Error: no import for you!") + + +def test_gssapi_without_sasl(monkeypatch, ): + monkeypatch.setattr("ldap.SASL_AVAIL", 0) + + with pytest.raises(AnsibleLookupError) as err: + lookup_loader.get('community.windows.laps_password').run(["host"], domain="test") + + assert str(err.value) == "Cannot use auth=gssapi when SASL is not configured with the local LDAP install" + + +def test_simple_auth_without_credentials(): + with pytest.raises(AnsibleLookupError) as err: + lookup_loader.get('community.windows.laps_password').run(["host"], domain="test", auth="simple") + + assert str(err.value) == "The username and password values are required when auth=simple" + + +def test_gssapi_auth_with_credentials(): + with pytest.raises(AnsibleLookupError) as err: + lookup_loader.get('community.windows.laps_password').run(["host"], domain="test", auth="gssapi", username="u", + password="p") + + assert str(err.value) == "Explicit credentials are not supported when auth='gssapi'. Call kinit outside of Ansible" + + +def test_not_encrypted_without_override(): + with pytest.raises(AnsibleLookupError) as err: + lookup_loader.get('community.windows.laps_password').run(["host"], domain="dc01", auth="simple", + username="test", password="test") + + assert str(err.value) == "Current configuration will result in plaintext traffic exposing credentials. Set " \ + "auth=gssapi, scheme=ldaps, start_tls=True, or allow_plaintext=True to continue" + + +def test_ldaps_without_tls(monkeypatch, ): + monkeypatch.setattr("ldap.TLS_AVAIL", 0) + + with pytest.raises(AnsibleLookupError) as err: + lookup_loader.get('community.windows.laps_password').run(["host"], domain="dc01", scheme="ldaps") + + assert str(err.value) == "Cannot use TLS as the local LDAP installed has not been configured to support it" + + +def test_start_tls_without_tls(monkeypatch, ): + monkeypatch.setattr("ldap.TLS_AVAIL", 0) + + with pytest.raises(AnsibleLookupError) as err: + lookup_loader.get('community.windows.laps_password').run(["host"], domain="dc01", start_tls=True) + + assert str(err.value) == "Cannot use TLS as the local LDAP installed has not been configured to support it" + + +def test_normal_run(monkeypatch, laps_password): + def get_laps_password(conn, cn, search_base): + return "CN=%s,%s" % (cn, search_base) + + mock_ldap = MagicMock() + mock_ldap.return_value.read_rootdse_s.return_value = {"defaultNamingContext": ["DC=domain,DC=com"]} + monkeypatch.setattr("ldap.initialize", mock_ldap) + + mock_get_laps_password = MagicMock(side_effect=get_laps_password) + monkeypatch.setattr(laps_password, "get_laps_password", mock_get_laps_password) + + actual = lookup_loader.get('community.windows.laps_password').run(["host1", "host2"], domain="dc01") + assert actual == ["CN=host1,DC=domain,DC=com", "CN=host2,DC=domain,DC=com"] + + # Verify the call count to get_laps_password + assert mock_get_laps_password.call_count == 2 + + # Verify the initialize() method call + assert mock_ldap.call_count == 1 + assert mock_ldap.call_args[0] == ("ldap://dc01:389",) + assert mock_ldap.call_args[1] == {"bytes_mode": False} + + # Verify the number of calls made to the mocked LDAP object + assert mock_ldap.mock_calls[1][0] == "().set_option" + assert mock_ldap.mock_calls[1][1] == (FakeLdap.OPT_PROTOCOL_VERSION, 3) + + assert mock_ldap.mock_calls[2][0] == "().set_option" + assert mock_ldap.mock_calls[2][1] == (FakeLdap.OPT_REFERRALS, 0) + + assert mock_ldap.mock_calls[3][0] == '().sasl_gssapi_bind_s' + assert mock_ldap.mock_calls[3][1] == () + + assert mock_ldap.mock_calls[4][0] == "().read_rootdse_s" + assert mock_ldap.mock_calls[4][1] == () + + assert mock_ldap.mock_calls[5][0] == "().unbind_s" + assert mock_ldap.mock_calls[5][1] == () + + +def test_run_with_simple_auth_and_search_base(monkeypatch, laps_password): + def get_laps_password(conn, cn, search_base): + return "CN=%s,%s" % (cn, search_base) + + mock_ldap = MagicMock() + monkeypatch.setattr("ldap.initialize", mock_ldap) + + mock_get_laps_password = MagicMock(side_effect=get_laps_password) + monkeypatch.setattr(laps_password, "get_laps_password", mock_get_laps_password) + + actual = lookup_loader.get('community.windows.laps_password').run(["host1", "host2"], domain="dc01", auth="simple", + username="user", password="pass", + allow_plaintext=True, + search_base="OU=Workstations,DC=domain,DC=com") + assert actual == ["CN=host1,OU=Workstations,DC=domain,DC=com", "CN=host2,OU=Workstations,DC=domain,DC=com"] + + # Verify the call count to get_laps_password + assert mock_get_laps_password.call_count == 2 + + # Verify the initialize() method call + assert mock_ldap.call_count == 1 + assert mock_ldap.call_args[0] == ("ldap://dc01:389",) + assert mock_ldap.call_args[1] == {"bytes_mode": False} + + # Verify the number of calls made to the mocked LDAP object + assert mock_ldap.mock_calls[1][0] == "().set_option" + assert mock_ldap.mock_calls[1][1] == (FakeLdap.OPT_PROTOCOL_VERSION, 3) + + assert mock_ldap.mock_calls[2][0] == "().set_option" + assert mock_ldap.mock_calls[2][1] == (FakeLdap.OPT_REFERRALS, 0) + + assert mock_ldap.mock_calls[3][0] == '().bind_s' + assert mock_ldap.mock_calls[3][1] == (u"user", u"pass") + + assert mock_ldap.mock_calls[4][0] == "().unbind_s" + assert mock_ldap.mock_calls[4][1] == () + + +@pytest.mark.parametrize("kwargs, expected", [ + [{"domain": "dc01"}, "ldap://dc01:389"], + [{"domain": "dc02", "port": 1234}, "ldap://dc02:1234"], + [{"domain": "dc03", "scheme": "ldaps"}, "ldaps://dc03:636"], + # Verifies that an explicit URI ignores port and scheme + [{"domain": "ldap://dc04", "port": 1234, "scheme": "ldaps"}, "ldap://dc04"], +]) +def test_uri_options(monkeypatch, kwargs, expected): + mock_ldap = MagicMock() + monkeypatch.setattr("ldap.initialize", mock_ldap) + + lookup_loader.get('community.windows.laps_password').run([], **kwargs) + + assert mock_ldap.call_count == 1 + assert mock_ldap.call_args[0] == (expected,) + assert mock_ldap.call_args[1] == {"bytes_mode": False} + + +@pytest.mark.parametrize("validate, expected", [ + ["never", FakeLdap.OPT_X_TLS_NEVER], + ["allow", FakeLdap.OPT_X_TLS_ALLOW], + ["try", FakeLdap.OPT_X_TLS_TRY], + ["demand", FakeLdap.OPT_X_TLS_DEMAND], +]) +def test_certificate_validation(monkeypatch, validate, expected): + mock_ldap_option = MagicMock() + monkeypatch.setattr(FakeLdap, "set_option", mock_ldap_option) + + mock_ldap = MagicMock() + monkeypatch.setattr("ldap.initialize", mock_ldap) + + lookup_loader.get('community.windows.laps_password').run([], domain="dc01", start_tls=True, + validate_certs=validate) + + assert mock_ldap_option.mock_calls[0][1] == (FakeLdap.OPT_X_TLS_REQUIRE_CERT, expected) + + assert mock_ldap.mock_calls[3][0] == "().start_tls_s" + assert mock_ldap.mock_calls[3][1] == () + + assert mock_ldap.mock_calls[4][0] == "().sasl_gssapi_bind_s" + assert mock_ldap.mock_calls[4][1] == () + + +def test_certificate_validate_with_custom_cacert(monkeypatch): + mock_ldap_option = MagicMock() + monkeypatch.setattr(FakeLdap, "set_option", mock_ldap_option) + + mock_ldap = MagicMock() + monkeypatch.setattr("ldap.initialize", mock_ldap) + monkeypatch.setattr(os.path, 'exists', lambda x: True) + + lookup_loader.get('community.windows.laps_password').run([], domain="dc01", scheme="ldaps", + cacert_file="cacert.pem") + + assert mock_ldap_option.mock_calls[0][1] == (FakeLdap.OPT_X_TLS_REQUIRE_CERT, FakeLdap.OPT_X_TLS_DEMAND) + assert mock_ldap_option.mock_calls[1][1] == (FakeLdap.OPT_X_TLS_CACERTFILE, u"cacert.pem") + + assert mock_ldap.mock_calls[3][0] == "().sasl_gssapi_bind_s" + assert mock_ldap.mock_calls[3][1] == () + + +def test_certificate_validate_with_custom_cacert_fail(monkeypatch): + def set_option(self, key, value): + if key == FakeLdap.OPT_X_TLS_CACERTFILE: + raise ValueError("set_option() failed") + + monkeypatch.setattr(FakeLdap, "set_option", set_option) + monkeypatch.setattr(os.path, 'exists', lambda x: True) + + with pytest.raises(AnsibleLookupError) as err: + lookup_loader.get('community.windows.laps_password').run([], domain="dc01", scheme="ldaps", + cacert_file="cacert.pem") + + assert str(err.value) == "Failed to set path to cacert file, this is a known issue with older OpenLDAP " \ + "libraries on the host. Update OpenLDAP and reinstall python-ldap to continue" + + +@pytest.mark.parametrize("path", [ + "cacert.pem", + "~/.certs/cacert.pem", + "~/.certs/$USER/cacert.pem", +]) +def test_certificate_invalid_path(monkeypatch, path): + lookup = lookup_loader.get('community.windows.laps_password') + monkeypatch.setattr(os.path, 'exists', lambda x: False) + expected_path = os.path.expanduser(os.path.expandvars(path)) + + with pytest.raises(AnsibleLookupError) as err: + lookup.run([], domain="dc01", scheme="ldaps", cacert_file=path) + + assert str(err.value) == "The cacert_file specified '%s' does not exist" % expected_path + + +def test_simple_auth_with_ldaps(monkeypatch): + mock_ldap_option = MagicMock() + monkeypatch.setattr(FakeLdap, "set_option", mock_ldap_option) + + mock_ldap = MagicMock() + monkeypatch.setattr("ldap.initialize", mock_ldap) + + lookup_loader.get('community.windows.laps_password').run([], domain="dc01", scheme="ldaps", auth="simple", + username="user", password="pass") + + assert mock_ldap_option.mock_calls[0][1] == (FakeLdap.OPT_X_TLS_REQUIRE_CERT, FakeLdap.OPT_X_TLS_DEMAND) + + assert mock_ldap.mock_calls[3][0] == '().bind_s' + assert mock_ldap.mock_calls[3][1] == (u"user", u"pass") + + assert mock_ldap.mock_calls[4][0] == "().read_rootdse_s" + assert mock_ldap.mock_calls[4][1] == () + + +def test_simple_auth_with_start_tls(monkeypatch): + mock_ldap_option = MagicMock() + monkeypatch.setattr(FakeLdap, "set_option", mock_ldap_option) + + mock_ldap = MagicMock() + monkeypatch.setattr("ldap.initialize", mock_ldap) + + lookup_loader.get('community.windows.laps_password').run([], domain="dc01", start_tls=True, auth="simple", + username="user", password="pass") + + assert mock_ldap_option.mock_calls[0][1] == (FakeLdap.OPT_X_TLS_REQUIRE_CERT, FakeLdap.OPT_X_TLS_DEMAND) + + assert mock_ldap.mock_calls[3][0] == "().start_tls_s" + assert mock_ldap.mock_calls[3][1] == () + + assert mock_ldap.mock_calls[4][0] == '().bind_s' + assert mock_ldap.mock_calls[4][1] == (u"user", u"pass") + + assert mock_ldap.mock_calls[5][0] == "().read_rootdse_s" + assert mock_ldap.mock_calls[5][1] == () + + +def test_start_tls_ldap_error(monkeypatch): + mock_ldap = MagicMock() + mock_ldap.return_value.start_tls_s.side_effect = FakeLDAPError("fake error") + monkeypatch.setattr("ldap.initialize", mock_ldap) + + with pytest.raises(AnsibleLookupError) as err: + lookup_loader.get('community.windows.laps_password').run([], domain="dc01", start_tls=True) + + assert str(err.value) == "Failed to send StartTLS to LDAP host 'ldap://dc01:389': fake error" + + +def test_simple_bind_ldap_error(monkeypatch): + mock_ldap = MagicMock() + mock_ldap.return_value.bind_s.side_effect = FakeLDAPError("fake error") + monkeypatch.setattr("ldap.initialize", mock_ldap) + + with pytest.raises(AnsibleLookupError) as err: + lookup_loader.get('community.windows.laps_password').run([], domain="dc01", auth="simple", username="user", + password="pass", allow_plaintext=True) + + assert str(err.value) == "Failed to simple bind against LDAP host 'ldap://dc01:389': fake error" + + +def test_sasl_bind_ldap_error(monkeypatch): + mock_ldap = MagicMock() + mock_ldap.return_value.sasl_gssapi_bind_s.side_effect = FakeLDAPError("fake error") + monkeypatch.setattr("ldap.initialize", mock_ldap) + + with pytest.raises(AnsibleLookupError) as err: + lookup_loader.get('community.windows.laps_password').run([], domain="dc01") + + assert str(err.value) == "Failed to do a sasl bind against LDAP host 'ldap://dc01:389': fake error" + + +def test_sasl_bind_ldap_no_mechs_error(monkeypatch): + mock_ldap = MagicMock() + mock_ldap.return_value.sasl_gssapi_bind_s.side_effect = FakeLDAPAuthUnknownError("no mechs") + monkeypatch.setattr("ldap.initialize", mock_ldap) + + with pytest.raises(AnsibleLookupError) as err: + lookup_loader.get('community.windows.laps_password').run([], domain="dc01") + + assert str(err.value) == "Failed to do a sasl bind against LDAP host 'ldap://dc01:389', the GSSAPI mech is " \ + "not installed: no mechs" + + +def test_get_password_valid(laps_password): + mock_conn = MagicMock() + mock_conn.search_s.return_value = [ + ("CN=server,DC=domain,DC=local", + {"ms-Mcs-AdmPwd": ["pass"], "distinguishedName": ["CN=server,DC=domain,DC=local"]}), + # Replicates the 3 extra entries AD returns that aren't server objects + (None, ["ldap://ForestDnsZones.domain.com/DC=ForestDnsZones,DC=domain,DC=com"]), + (None, ["ldap://DomainDnsZones.domain.com/DC=DomainDnsZones,DC=domain,DC=com"]), + (None, ["ldap://domain.com/CN=Configuration,DC=domain,DC=com"]), + ] + + actual = laps_password.get_laps_password(mock_conn, "server", "DC=domain,DC=local") + assert actual == "pass" + + assert len(mock_conn.method_calls) == 1 + assert mock_conn.method_calls[0][0] == "search_s" + assert mock_conn.method_calls[0][1] == ("DC=domain,DC=local", FakeLdap.SCOPE_SUBTREE, + "(&(objectClass=computer)(CN=server))") + assert mock_conn.method_calls[0][2] == {"attrlist": ["distinguishedName", "ms-Mcs-AdmPwd"]} + + +def test_get_password_laps_not_configured(laps_password): + mock_conn = MagicMock() + mock_conn.search_s.return_value = [ + ("CN=server,DC=domain,DC=local", {"distinguishedName": ["CN=server,DC=domain,DC=local"]}), + (None, ["ldap://ForestDnsZones.domain.com/DC=ForestDnsZones,DC=domain,DC=com"]), + (None, ["ldap://DomainDnsZones.domain.com/DC=DomainDnsZones,DC=domain,DC=com"]), + (None, ["ldap://domain.com/CN=Configuration,DC=domain,DC=com"]), + ] + + with pytest.raises(AnsibleLookupError) as err: + laps_password.get_laps_password(mock_conn, "server2", "DC=test,DC=local") + assert str(err.value) == \ + "The server 'CN=server,DC=domain,DC=local' did not have the LAPS attribute 'ms-Mcs-AdmPwd'" + + assert len(mock_conn.method_calls) == 1 + assert mock_conn.method_calls[0][0] == "search_s" + assert mock_conn.method_calls[0][1] == ("DC=test,DC=local", FakeLdap.SCOPE_SUBTREE, + "(&(objectClass=computer)(CN=server2))") + assert mock_conn.method_calls[0][2] == {"attrlist": ["distinguishedName", "ms-Mcs-AdmPwd"]} + + +def test_get_password_no_results(laps_password): + mock_conn = MagicMock() + mock_conn.search_s.return_value = [ + (None, ["ldap://ForestDnsZones.domain.com/DC=ForestDnsZones,DC=domain,DC=com"]), + (None, ["ldap://DomainDnsZones.domain.com/DC=DomainDnsZones,DC=domain,DC=com"]), + (None, ["ldap://domain.com/CN=Configuration,DC=domain,DC=com"]), + ] + + with pytest.raises(AnsibleLookupError) as err: + laps_password.get_laps_password(mock_conn, "server", "DC=domain,DC=local") + assert str(err.value) == "Failed to find the server 'server' in the base 'DC=domain,DC=local'" + + assert len(mock_conn.method_calls) == 1 + assert mock_conn.method_calls[0][0] == "search_s" + assert mock_conn.method_calls[0][1] == ("DC=domain,DC=local", FakeLdap.SCOPE_SUBTREE, + "(&(objectClass=computer)(CN=server))") + assert mock_conn.method_calls[0][2] == {"attrlist": ["distinguishedName", "ms-Mcs-AdmPwd"]} + + +def test_get_password_multiple_results(laps_password): + mock_conn = MagicMock() + mock_conn.search_s.return_value = [ + ("CN=server,OU=Workstations,DC=domain,DC=local", + {"ms-Mcs-AdmPwd": ["pass"], "distinguishedName": ["CN=server,OU=Workstations,DC=domain,DC=local"]}), + ("CN=server,OU=Servers,DC=domain,DC=local", + {"ms-Mcs-AdmPwd": ["pass"], "distinguishedName": ["CN=server,OU=Servers,DC=domain,DC=local"]}), + (None, ["ldap://ForestDnsZones.domain.com/DC=ForestDnsZones,DC=domain,DC=com"]), + (None, ["ldap://DomainDnsZones.domain.com/DC=DomainDnsZones,DC=domain,DC=com"]), + (None, ["ldap://domain.com/CN=Configuration,DC=domain,DC=com"]), + ] + + with pytest.raises(AnsibleLookupError) as err: + laps_password.get_laps_password(mock_conn, "server", "DC=domain,DC=local") + assert str(err.value) == \ + "Found too many results for the server 'server' in the base 'DC=domain,DC=local'. Specify a more explicit " \ + "search base for the server required. Found servers 'CN=server,OU=Workstations,DC=domain,DC=local', " \ + "'CN=server,OU=Servers,DC=domain,DC=local'" + + assert len(mock_conn.method_calls) == 1 + assert mock_conn.method_calls[0][0] == "search_s" + assert mock_conn.method_calls[0][1] == ("DC=domain,DC=local", FakeLdap.SCOPE_SUBTREE, + "(&(objectClass=computer)(CN=server))") + assert mock_conn.method_calls[0][2] == {"attrlist": ["distinguishedName", "ms-Mcs-AdmPwd"]} diff --git a/ansible_collections/community/windows/tests/unit/requirements.txt b/ansible_collections/community/windows/tests/unit/requirements.txt new file mode 100644 index 000000000..c4e4edb59 --- /dev/null +++ b/ansible_collections/community/windows/tests/unit/requirements.txt @@ -0,0 +1,3 @@ +setuptools > 0.6 # pytest-xdist installed via requirements does not work with very old setuptools (sanity_ok) +unittest2 ; python_version < '2.7' +importlib ; python_version < '2.7' diff --git a/ansible_collections/community/windows/tests/utils/shippable/sanity.sh b/ansible_collections/community/windows/tests/utils/shippable/sanity.sh new file mode 100755 index 000000000..f7165f06d --- /dev/null +++ b/ansible_collections/community/windows/tests/utils/shippable/sanity.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -o pipefail -eux + +if [ "${BASE_BRANCH:-}" ]; then + base_branch="origin/${BASE_BRANCH}" +else + base_branch="" +fi + +# shellcheck disable=SC2086 +ansible-test sanity --color -v --junit ${COVERAGE:+"$COVERAGE"} ${CHANGED:+"$CHANGED"} \ + --docker --base-branch "${base_branch}" --allow-disabled diff --git a/ansible_collections/community/windows/tests/utils/shippable/shippable.sh b/ansible_collections/community/windows/tests/utils/shippable/shippable.sh new file mode 100755 index 000000000..cca295366 --- /dev/null +++ b/ansible_collections/community/windows/tests/utils/shippable/shippable.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash + +set -o pipefail -eux + +declare -a args +IFS='/:' read -ra args <<< "$1" + +ansible_version="${args[0]}" +script="${args[1]}" + +function join { + local IFS="$1"; + shift; + echo "$*"; +} + +test="$(join / "${args[@]:1}")" + +docker images ansible/ansible +docker images quay.io/ansible/* +docker ps + +for container in $(docker ps --format '{{.Image}} {{.ID}}' | grep -v -e '^drydock/' -e '^quay.io/ansible/azure-pipelines-test-container:' | sed 's/^.* //'); do + docker rm -f "${container}" || true # ignore errors +done + +docker ps + +command -v python +python -V + +function retry +{ + # shellcheck disable=SC2034 + for repetition in 1 2 3; do + set +e + "$@" + result=$? + set -e + if [ ${result} == 0 ]; then + return ${result} + fi + echo "@* -> ${result}" + done + echo "Command '@*' failed 3 times!" + exit 1 +} + +command -v pip +pip --version +pip list --disable-pip-version-check +if [ "${ansible_version}" == "devel" ]; then + retry pip install https://github.com/ansible/ansible/archive/devel.tar.gz --disable-pip-version-check +else + retry pip install "https://github.com/ansible/ansible/archive/stable-${ansible_version}.tar.gz" --disable-pip-version-check +fi + +sudo chown "$(whoami)" "${PWD}/../../" + +export PYTHONIOENCODING='utf-8' + +if [ -n "${COVERAGE:-}" ]; then + # on-demand coverage reporting triggered by setting the COVERAGE environment variable to a non-empty value + export COVERAGE="--coverage" +elif [[ "${COMMIT_MESSAGE}" =~ ci_coverage ]]; then + # on-demand coverage reporting triggered by having 'ci_coverage' in the latest commit message + export COVERAGE="--coverage" +else + # on-demand coverage reporting disabled (default behavior, always-on coverage reporting remains enabled) + export COVERAGE="--coverage-check" +fi + +if [ -n "${COMPLETE:-}" ]; then + # disable change detection triggered by setting the COMPLETE environment variable to a non-empty value + export CHANGED="" +elif [[ "${COMMIT_MESSAGE}" =~ ci_complete ]]; then + # disable change detection triggered by having 'ci_complete' in the latest commit message + export CHANGED="" +else + # enable change detection (default behavior) + export CHANGED="--changed" +fi + +if [ "${IS_PULL_REQUEST:-}" == "true" ]; then + # run unstable tests which are targeted by focused changes on PRs + export UNSTABLE="--allow-unstable-changed" +else + # do not run unstable tests outside PRs + export UNSTABLE="" +fi + +if [[ "${COVERAGE:-}" == "--coverage" ]]; then + timeout=60 +else + timeout=50 +fi + +ansible-test env --dump --show --timeout "${timeout}" --color -v + +"tests/utils/shippable/${script}.sh" "${test}" diff --git a/ansible_collections/community/windows/tests/utils/shippable/units.sh b/ansible_collections/community/windows/tests/utils/shippable/units.sh new file mode 100755 index 000000000..bcf7a771a --- /dev/null +++ b/ansible_collections/community/windows/tests/utils/shippable/units.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -o pipefail -eux + +# shellcheck disable=SC2086 +ansible-test units --color -v --docker default ${COVERAGE:+"$COVERAGE"} ${CHANGED:+"$CHANGED"} + diff --git a/ansible_collections/community/windows/tests/utils/shippable/windows.sh b/ansible_collections/community/windows/tests/utils/shippable/windows.sh new file mode 100755 index 000000000..9b624e0b8 --- /dev/null +++ b/ansible_collections/community/windows/tests/utils/shippable/windows.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -o pipefail -eux + +declare -a args +IFS='/:' read -ra args <<< "$1" + +version="${args[1]}" + +if [ "${#args[0]}" -gt 2 ]; then + target="shippable/windows/group${args[2]}/" +else + target="shippable/windows/" +fi + +stage="${S:-prod}" +provider="${P:-default}" + +# shellcheck disable=SC2086 +ansible-test windows-integration --color -v --retry-on-error "${target}" ${COVERAGE:+"$COVERAGE"} ${CHANGED:+"$CHANGED"} ${UNSTABLE:+"$UNSTABLE"} \ + --windows "${version}" --docker default --remote-terminate always --remote-stage "${stage}" --remote-provider "${provider}" |