diff --git a/lib/ansible/modules/windows/win_mapped_drive.ps1 b/lib/ansible/modules/windows/win_mapped_drive.ps1 new file mode 100644 index 0000000000..ed5ac57603 --- /dev/null +++ b/lib/ansible/modules/windows/win_mapped_drive.ps1 @@ -0,0 +1,120 @@ +#!powershell +# This file is part of Ansible + +# Copyright (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#Requires -Module Ansible.ModuleUtils.Legacy.psm1 + +$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 + +$letter = Get-AnsibleParam -obj $params -name "letter" -type "str" -failifempty $true +$path = Get-AnsibleParam -obj $params -name "path" -type "path" +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent","present" +$username = Get-AnsibleParam -obj $params -name "username" -type "str" +$password = Get-AnsibleParam -obj $params -name "password" -type "str" + +$result = @{ + changed = $false +} + +if ($diff_mode) { + $result.diff = @{} +} + +if ($letter -notmatch "^[a-zA-z]{1}$") { + Fail-Json $result "letter must be a single letter from A-Z, was: $letter" +} + +Function Get-MappedDriveTarget($letter) { + # Get-PSDrive and Get-CimInstance doesn't work through WinRM + $target = $null + if (Test-Path -Path HKCU:\Network\$letter) { + $target = (Get-ItemProperty -Path HKCU:\Network\$letter -Name RemotePath).RemotePath + } + + return $target +} + +Function Remove-MappedDrive($letter) { + # Remove-PSDrive doesn't work through WinRM as it cannot view the mapped drives for the user + if (-not $check_mode) { + try { + &cmd.exe /c net use "$($letter):" /delete + } catch { + Fail-Json $result "failed to removed mapped drive $($letter): $($_.Exception.Message)" + } + } +} + +$existing_target = Get-MappedDriveTarget -letter $letter + +if ($state -eq "absent") { + if ($existing_target -ne $null) { + if ($path -ne $null) { + if ($existing_target -eq $path) { + Remove-MappedDrive -letter $letter + } else { + Fail-Json $result "did not delete mapped drive $letter, the target path is pointing to a different location at $existing_target" + } + } else { + Remove-MappedDrive -letter $letter + } + + $result.changed = $true + if ($diff_mode) { + $result.diff.prepared = "-$($letter): $existing_target" + } + } +} else { + if ($path -eq $null) { + Fail-Json $result "path must be set when creating a mapped drive" + } + + $extra_args = @{} + if ($username -ne $null) { + $sec_password = ConvertTo-SecureString -String $password -AsPlainText -Force + $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $username, $sec_password + $extra_args.Credential = $credential + } + + $physical_drives = Get-PSDrive -PSProvider "FileSystem" + if ($letter -in $physical_drives.Name) { + Fail-Json $result "failed to create mapped drive $letter, this letter is in use and is pointing to a non UNC path" + } + + if ($existing_target -ne $null) { + if ($existing_target -ne $path -or ($username -ne $null)) { + # the source path doesn't match or we are putting in a credential + Remove-MappedDrive -letter $letter + $result.changed = $true + + try { + New-PSDrive -Name $letter -PSProvider "FileSystem" -root $path -Persist -WhatIf:$check_mode @extra_args | Out-Null + } catch { + Fail-Json $result "failed to create mapped drive $letter pointed to $($path): $($_.Exception.Message)" + } + + if ($diff_mode) { + $result.diff.prepared = "-$($letter): $existing_target`n+$($letter): $path" + } + } + } else { + try { + New-PSDrive -Name $letter -PSProvider "FileSystem" -Root $path -Persist -WhatIf:$check_mode @extra_args | Out-Null + } catch { + Fail-Json $result "failed to create mapped drive $letter pointed to $($path): $($_.Exception.Message)" + } + + $result.changed = $true + if ($diff_mode) { + $result.diff.prepared = "+$($letter): $path" + } + } +} + +Exit-Json $result diff --git a/lib/ansible/modules/windows/win_mapped_drive.py b/lib/ansible/modules/windows/win_mapped_drive.py new file mode 100644 index 0000000000..1830bf2ee8 --- /dev/null +++ b/lib/ansible/modules/windows/win_mapped_drive.py @@ -0,0 +1,91 @@ +#!/usr/bin/python +# This file is part of Ansible + +# Copyright (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# this is a windows documentation stub, actual code lives in the .ps1 +# file of the same name + +ANSIBLE_METADATA = {'metadata_version': '1.0', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = r''' +--- +module: win_mapped_drive +version_added: '2.4' +short_description: maps a network drive for a user +description: +- Allows you to modify mapped network drives for individual users. +notes: +- This can only map a network drive for the current executing user and does not + allow you to set a default drive for all users of a system. Use other + Microsoft tools like GPOs to achieve this goal. +options: + letter: + description: + - The letter of the network path to map to. + - This letter must not already be in use with Windows. + required: yes + password: + description: + - The password for C(username). + path: + description: + - The UNC path to map the drive to. + - This is required if C(state=present). + - If C(state=absent) and path is not set, the module will delete the mapped + drive regardless of the target. + - If C(state=absent) and the path is set, the module will throw an error if + path does not match the target of the mapped drive. + state: + description: + - If C(state=present) will ensure the mapped drive exists. + - If C(state=absent) will ensure the mapped drive does not exist. + choices: [ absent, present ] + default: present + username: + description: + - Credentials to map the drive with. + - The username MUST include the domain or servername like SERVER\user, see + the example for more information. +author: +- Jordan Borean (@jborean93) +''' + +EXAMPLES = r''' +- name: create a mapped drive under Z + win_mapped_drive: + letter: Z + path: \\domain\appdata\accounting + +- name: delete any mapped drives under Z + win_mapped_drive: + letter: Z + state: absent + +- name: only delete the mapped drive Z if the paths match (error is thrown otherwise) + win_mapped_drive: + letter: Z + path: \\domain\appdata\accounting + state: absent + +- name: create mapped drive with local credentials + win_mapped_drive: + letter: M + path: \\SERVER\c$ + username: SERVER\Administrator + password: Password + +- name: create mapped drive with domain credentials + win_mapped_drive: + letter: M + path: \\domain\appdata\it + username: DOMAIN\IT + password: Password +''' + +RETURN = r''' +''' diff --git a/test/integration/targets/win_mapped_drive/aliases b/test/integration/targets/win_mapped_drive/aliases new file mode 100644 index 0000000000..10e03fc2bf --- /dev/null +++ b/test/integration/targets/win_mapped_drive/aliases @@ -0,0 +1 @@ +windows/ci/group1 diff --git a/test/integration/targets/win_mapped_drive/defaults/main.yml b/test/integration/targets/win_mapped_drive/defaults/main.yml new file mode 100644 index 0000000000..6e8ed002ed --- /dev/null +++ b/test/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/test/integration/targets/win_mapped_drive/tasks/main.yml b/test/integration/targets/win_mapped_drive/tasks/main.yml new file mode 100644 index 0000000000..09eb7f8f88 --- /dev/null +++ b/test/integration/targets/win_mapped_drive/tasks/main.yml @@ -0,0 +1,62 @@ +--- +# test setup +- name: gather facts required by the tests + setup: + +- 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 + 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 + 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 + 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}}' } + +- block: + # tests + - include_tasks: tests.yml + + # test cleanup + always: + - 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 + 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 + 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 + win_user: + name: '{{test_win_mapped_drive_temp_user}}' + state: absent diff --git a/test/integration/targets/win_mapped_drive/tasks/tests.yml b/test/integration/targets/win_mapped_drive/tasks/tests.yml new file mode 100644 index 0000000000..1868e988ea --- /dev/null +++ b/test/integration/targets/win_mapped_drive/tasks/tests.yml @@ -0,0 +1,272 @@ +--- +- name: fail with invalid path + win_mapped_drive: + letter: invalid + 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 != 'path must be set when creating a mapped drive' + +- 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 + 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|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 + win_command: 'net use {{test_win_mapped_drive_letter}}:' + register: create_drive_actual + +- name: assert create mapped drive + assert: + that: + - create_drive|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: + - not create_drive_again|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 + 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|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 + 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|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 + 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|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 + 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|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: + - not delete_drive_again|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 + 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|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 + win_command: 'net use {{test_win_mapped_drive_letter}}:' + register: map_with_credentials_actual + +- name: get username of mapped network drive with credentials + 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|changed + - map_with_credentials_actual.rc == 0 + - map_with_credential_actual_username.value == '{{ansible_hostname}}\\{{test_win_mapped_drive_temp_user}}' + +- 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: + - map_with_credentials_again|changed # we expect a change as it will just delete and recreate if credentials are passed + +- 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 + 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|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 + 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|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: + - not delete_without_path_again|changed