From 4651bcf561cfad29368aa15db017c3554f80e1cf Mon Sep 17 00:00:00 2001 From: Varun Chopra Date: Tue, 9 Apr 2019 02:15:04 +0530 Subject: [PATCH] Add win_format (#53925) * Add win_format * Some doc changes were missed * Fixes for ansible-test, additional assertion for check mode * Fix -WhatIf issues * Support for idempotency and changes to integration tests * Fix trailing whitespace * Fixes from review, and added check for non-empty volumes * Remove an extra line * Structural changes * Minor fixes for CI --- lib/ansible/modules/windows/win_format.ps1 | 184 ++++++++++++++++++ lib/ansible/modules/windows/win_format.py | 100 ++++++++++ test/integration/targets/win_format/aliases | 3 + .../targets/win_format/meta/main.yml | 2 + .../targets/win_format/tasks/main.yml | 7 + .../targets/win_format/tasks/pre_test.yml | 21 ++ .../targets/win_format/tasks/tests.yml | 138 +++++++++++++ .../templates/partition_creation_script.j2 | 11 ++ .../templates/partition_deletion_script.j2 | 3 + 9 files changed, 469 insertions(+) create mode 100644 lib/ansible/modules/windows/win_format.ps1 create mode 100644 lib/ansible/modules/windows/win_format.py create mode 100644 test/integration/targets/win_format/aliases create mode 100644 test/integration/targets/win_format/meta/main.yml create mode 100644 test/integration/targets/win_format/tasks/main.yml create mode 100644 test/integration/targets/win_format/tasks/pre_test.yml create mode 100644 test/integration/targets/win_format/tasks/tests.yml create mode 100644 test/integration/targets/win_format/templates/partition_creation_script.j2 create mode 100644 test/integration/targets/win_format/templates/partition_deletion_script.j2 diff --git a/lib/ansible/modules/windows/win_format.ps1 b/lib/ansible/modules/windows/win_format.ps1 new file mode 100644 index 00000000000..6302ad8754c --- /dev/null +++ b/lib/ansible/modules/windows/win_format.ps1 @@ -0,0 +1,184 @@ +#!powershell + +# Copyright: (c) 2019, Varun Chopra (@chopraaa) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic +#AnsibleRequires -OSVersion 6.2 + +Set-StrictMode -Version 2 + +$ErrorActionPreference = "Stop" + +$spec = @{ + options = @{ + drive_letter = @{ type = "str" } + path = @{ type = "str" } + label = @{ type = "str" } + new_label = @{ type = "str" } + file_system = @{ type = "str"; choices = "ntfs", "refs", "exfat", "fat32", "fat" } + allocation_unit_size = @{ type = "int" } + large_frs = @{ type = "bool" } + full = @{ type = "bool"; default = $false } + compress = @{ type = "bool" } + integrity_streams = @{ type = "bool" } + force = @{ type = "bool"; default = $false } + } + mutually_exclusive = @( + ,@('drive_letter', 'path', 'label') + ) + required_one_of = @( + ,@('drive_letter', 'path', 'label') + ) + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$drive_letter = $module.Params.drive_letter +$path = $module.Params.path +$label = $module.Params.label +$new_label = $module.Params.new_label +$file_system = $module.Params.file_system +$allocation_unit_size = $module.Params.allocation_unit_size +$large_frs = $module.Params.large_frs +$full_format = $module.Params.full +$compress_volume = $module.Params.compress +$integrity_streams = $module.Params.integrity_streams +$force_format = $module.Params.force + +# Some pre-checks +if ($null -ne $drive_letter -and $drive_letter -notmatch "^[a-zA-Z]$") { + $module.FailJson("The parameter drive_letter should be a single character A-Z") +} +if ($integrity_streams -eq $true -and $file_system -ne "refs") { + $module.FailJson("Integrity streams can be enabled only on ReFS volumes. You specified: $($file_system)") +} +if ($compress_volume -eq $true) { + if ($file_system -eq "ntfs") { + if ($null -ne $allocation_unit_size -and $allocation_unit_size -gt 4096) { + $module.FailJson("NTFS compression is not supported for allocation unit sizes above 4096") + } + } + else { + $module.FailJson("Compression can be enabled only on NTFS volumes. You specified: $($file_system)") + } +} + +function Get-AnsibleVolume { + param( + $DriveLetter, + $Path, + $Label + ) + + if ($null -ne $DriveLetter) { + try { + $volume = Get-Volume -DriveLetter $DriveLetter + } catch { + $module.FailJson("There was an error retrieving the volume using drive_letter $($DriveLetter): $($_.Exception.Message)", $_) + } + } + elseif ($null -ne $Path) { + try { + $volume = Get-Volume -Path $Path + } catch { + $module.FailJson("There was an error retrieving the volume using path $($Path): $($_.Exception.Message)", $_) + } + } + elseif ($null -ne $Label) { + try { + $volume = Get-Volume -FileSystemLabel $Label + } catch { + $module.FailJson("There was an error retrieving the volume using label $($Label): $($_.Exception.Message)", $_) + } + } + else { + $module.FailJson("Unable to locate volume: drive_letter, path and label were not specified") + } + + return $volume +} + +function Format-AnsibleVolume { + param( + $Path, + $Label, + $FileSystem, + $Full, + $UseLargeFRS, + $Compress, + $SetIntegrityStreams + ) + $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) + } + + 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_partition = Get-Partition -Volume $ansible_volume + +foreach ($access_path in $ansible_partition.AccessPaths) { + if ($access_path -ne $Path) { + $files_in_volume = (Get-ChildItem -LiteralPath $access_path -ErrorAction SilentlyContinue | Measure-Object).Count + + if (-not $force_format -and $files_in_volume -gt 0) { + $module.FailJson("Force format must be specified to format non-pristine volumes") + } else { + if (-not $force_format -and + -not $null -eq $file_system -and + -not [string]::IsNullOrEmpty($ansible_file_system) -and + $file_system -ne $ansible_file_system) { + $module.FailJson("Force format must be specified since target file system: $($file_system) is different from the current file system of the volume: $($ansible_file_system.ToLower())") + } else { + $pristine = $true + } + } + } +} + +if ($force_format) { + if (-not $module.CheckMode) { + Format-AnsibleVolume -Path $ansible_volume.Path -Full $full_format -Label $new_label -FileSystem $file_system -SetIntegrityStreams $integrity_streams -UseLargeFRS $large_frs -Compress $compress_volume + } + $module.Result.changed = $true +} +else { + if ($pristine) { + if ($null -eq $new_label) { + $new_label = $ansible_volume.FileSystemLabel + } + # Conditions for formatting + if ($ansible_volume_size -eq 0 -or + $ansible_volume.FileSystemLabel -ne $new_label) { + if (-not $module.CheckMode) { + Format-AnsibleVolume -Path $ansible_volume.Path -Full $full_format -Label $new_label -FileSystem $file_system -SetIntegrityStreams $integrity_streams -UseLargeFRS $large_frs -Compress $compress_volume + } + $module.Result.changed = $true + } + } +} + +$module.ExitJson() diff --git a/lib/ansible/modules/windows/win_format.py b/lib/ansible/modules/windows/win_format.py new file mode 100644 index 00000000000..8b85cce8131 --- /dev/null +++ b/lib/ansible/modules/windows/win_format.py @@ -0,0 +1,100 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Varun Chopra (@chopraaa) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = r''' +module: win_format +version_added: '2.8' +short_description: Formats an existing volume or a new volume on an existing partition on Windows +description: + - The M(win_format) module formats an existing volume or a new volume on an existing partition on Windows +options: + drive_letter: + description: + - Used to specify the drive letter of the volume to be formatted. + type: str + path: + description: + - Used to specify the path to the volume to be formatted. + type: str + label: + description: + - Used to specify the label of the volume to be formatted. + type: str + new_label: + description: + - Used to specify the new file system label of the formatted volume. + type: str + file_system: + description: + - Used to specify the file system to be used when formatting the target volume. + type: str + choices: [ ntfs, refs, exfat, fat32, fat ] + allocation_unit_size: + description: + - Specifies the cluster size to use when formatting the volume. + - If no cluster size is specified when you format a partition, defaults are selected based on + the size of the partition. + type: int + large_frs: + description: + - Specifies that large File Record System (FRS) should be used. + type: bool + compress: + description: + - Enable compression on the resulting NTFS volume. + - NTFS compression is not supported where I(allocation_unit_size) is more than 4096. + type: bool + integrity_streams: + description: + - Enable integrity streams on the resulting ReFS volume. + type: bool + full: + description: + - A full format writes to every sector of the disk, takes much longer to perform than the + default (quick) format, and is not recommended on storage that is thinly provisioned. + - Specify C(true) for full format. + type: bool + force: + description: + - Specify if formatting should be forced for volumes that are not created from new partitions + or if the source and target file system are different. + type: bool +notes: + - One of three parameters (I(drive_letter), I(path) and I(label)) are mandatory to identify the target + volume but more than one cannot be specified at the same time. + - This module is idempotent if I(force) is not specified and file system labels remain preserved. + - For more information, see U(https://docs.microsoft.com/en-us/previous-versions/windows/desktop/stormgmt/format-msft-volume) +seealso: + - module: win_disk_facts + - module: win_partition +author: + - Varun Chopra (@chopraaa) +''' + +EXAMPLES = r''' +- name: Create a partition with drive letter D and size 5 GiB + win_partition: + drive_letter: D + partition_size: 5 GiB + disk_number: 1 + +- name: Full format the newly created partition as NTFS and label it + win_format: + drive_letter: D + file_system: NTFS + new_label: Formatted + full: True +''' + +RETURN = r''' +# +''' diff --git a/test/integration/targets/win_format/aliases b/test/integration/targets/win_format/aliases new file mode 100644 index 00000000000..3aa71f86abd --- /dev/null +++ b/test/integration/targets/win_format/aliases @@ -0,0 +1,3 @@ +shippable/windows/group4 +skip/windows/2008 +skip/windows/2008-R2 diff --git a/test/integration/targets/win_format/meta/main.yml b/test/integration/targets/win_format/meta/main.yml new file mode 100644 index 00000000000..9f37e96cd90 --- /dev/null +++ b/test/integration/targets/win_format/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_remote_tmp_dir diff --git a/test/integration/targets/win_format/tasks/main.yml b/test/integration/targets/win_format/tasks/main.yml new file mode 100644 index 00000000000..de773469df2 --- /dev/null +++ b/test/integration/targets/win_format/tasks/main.yml @@ -0,0 +1,7 @@ +--- +- name: Check if Format-Volume is supported + 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/test/integration/targets/win_format/tasks/pre_test.yml b/test/integration/targets/win_format/tasks/pre_test.yml new file mode 100644 index 00000000000..edc59ae52c5 --- /dev/null +++ b/test/integration/targets/win_format/tasks/pre_test.yml @@ -0,0 +1,21 @@ +--- +- set_fact: + AnsibleVhdx: '{{ remote_tmp_dir }}\AnsiblePart.vhdx' + +- name: Copy VHDX scripts + win_template: + src: "{{ item.src }}" + dest: '{{ remote_tmp_dir }}\{{ item.dest }}' + loop: + - { src: partition_creation_script.j2, dest: partition_creation_script.txt } + - { src: partition_deletion_script.j2, dest: partition_deletion_script.txt } + +- name: Create partition + win_command: diskpart.exe /s {{ remote_tmp_dir }}\partition_creation_script.txt + +- name: Run tests + block: + - include: tests.yml + always: + - name: Detach disk + win_command: diskpart.exe /s {{ remote_tmp_dir }}\partition_deletion_script.txt diff --git a/test/integration/targets/win_format/tasks/tests.yml b/test/integration/targets/win_format/tasks/tests.yml new file mode 100644 index 00000000000..1db4576cfef --- /dev/null +++ b/test/integration/targets/win_format/tasks/tests.yml @@ -0,0 +1,138 @@ +--- +- 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 + 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 + register: format_result_check + check_mode: True + +- win_shell: $AnsiPart = Get-Partition -DriveLetter T; $AnsiVol = Get-Volume -DriveLetter T; "$($AnsiPart.Size),$($AnsiVol.Size),$($AnsiVol.FileSystemLabel)" + register: formatted_value_result_check + +- name: Fully format volume and assign label + win_format: + drive_letter: T + new_label: Formatted + full: True + register: format_result + +- win_shell: $AnsiPart = Get-Partition -DriveLetter T; $AnsiVol = Get-Volume -DriveLetter T; "$($AnsiPart.Size),$($AnsiVol.Size),$($AnsiVol.FileSystemLabel)" + 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,2096033792,Formatted" + +- 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 + 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 }}" + 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.msg == "Force format must be specified to format non-pristine volumes"' + - format_volume_with_force is changed diff --git a/test/integration/targets/win_format/templates/partition_creation_script.j2 b/test/integration/targets/win_format/templates/partition_creation_script.j2 new file mode 100644 index 00000000000..8e47fda95ba --- /dev/null +++ b/test/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/test/integration/targets/win_format/templates/partition_deletion_script.j2 b/test/integration/targets/win_format/templates/partition_deletion_script.j2 new file mode 100644 index 00000000000..c2be9cd1446 --- /dev/null +++ b/test/integration/targets/win_format/templates/partition_deletion_script.j2 @@ -0,0 +1,3 @@ +select vdisk file="{{ AnsibleVhdx }}" + +detach vdisk