├── .gitignore ├── .vscode └── launch.json ├── CleanAndCopyVhd └── Optimize-FslDisk.ps1 ├── LICENSE ├── Mount-FslDisk ├── Dismount-FslDisk.ps1 ├── Mount-FslDisk.ps1 └── Tests │ ├── Dismount-FslDisk.Tests.ps1 │ ├── Mount-FslDisk.Tests.ps1 │ └── createDiskForTest.ps1 ├── Move-FslOutlookFolder └── MoveFslOutlookFolder.ps1 ├── README.md ├── Rename-FslDisk ├── Functions │ ├── Rename-FslDisk.ps1 │ └── Rename-SingleDisk.ps1 ├── Release │ └── Rename-FslDisk.ps1 └── build.ps1 ├── Resize-FslDisk ├── Functions │ ├── Resize-FslDisk.ps1 │ └── Tests │ │ └── Resize-FslDisk.Tests.ps1 ├── Release │ └── Resize-FslDisk.ps1 └── build.ps1 └── Shrink-FslDisk └── Shrink-FslDisk.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | *.zip 2 | Write-Log.ps1 3 | Add-FslTestVHDs.ps1 4 | start.ps1 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "PowerShell", 9 | "request": "launch", 10 | "name": "PowerShell Launch Current File", 11 | "script": "${file}", 12 | "args": [], 13 | "cwd": "${file}" 14 | }, 15 | { 16 | "type": "PowerShell", 17 | "request": "launch", 18 | "name": "PowerShell Launch Current File in Temporary Console", 19 | "script": "${file}", 20 | "args": [], 21 | "cwd": "${file}", 22 | "createTemporaryIntegratedConsole": true 23 | }, 24 | { 25 | "type": "PowerShell", 26 | "request": "launch", 27 | "name": "PowerShell Launch Current File w/Args Prompt", 28 | "script": "${file}", 29 | "args": [ 30 | "${command:SpecifyScriptArgs}" 31 | ], 32 | "cwd": "${file}" 33 | }, 34 | { 35 | "type": "PowerShell", 36 | "request": "attach", 37 | "name": "PowerShell Attach to Host Process", 38 | "processId": "${command:PickPSHostProcess}", 39 | "runspaceId": 1 40 | }, 41 | { 42 | "type": "PowerShell", 43 | "request": "launch", 44 | "name": "PowerShell Interactive Session", 45 | "cwd": "" 46 | } 47 | ] 48 | } -------------------------------------------------------------------------------- /CleanAndCopyVhd/Optimize-FslDisk.ps1: -------------------------------------------------------------------------------- 1 | #requires -Module 'ActiveDirectory' 2 | 3 | function Remove-FslMultiOst { 4 | [CmdletBinding()] 5 | 6 | Param ( 7 | [Parameter( 8 | Position = 0, 9 | ValuefromPipelineByPropertyName = $true, 10 | ValuefromPipeline = $true, 11 | Mandatory = $true 12 | )] 13 | [System.String]$Path 14 | ) 15 | 16 | BEGIN { 17 | Set-StrictMode -Version Latest 18 | } # Begin 19 | PROCESS { 20 | #Write-Log "Getting ost files from $Path" 21 | $ost = Get-ChildItem -Path (Join-Path $Path *.ost) 22 | if ($null -eq $ost) { 23 | #Write-log -level Warn "Did not find any ost files in $Path" 24 | $ostDelNum = 0 25 | } 26 | else { 27 | 28 | $count = $ost | Measure-Object 29 | 30 | if ($count.Count -gt 1) { 31 | 32 | $mailboxes = $ost.BaseName.trimend('(', ')', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0') | Group-Object | Select-Object -ExpandProperty Name 33 | 34 | foreach ($mailbox in $mailboxes) { 35 | $mailboxOst = $ost | Where-Object {$_.BaseName.StartsWith($mailbox)} 36 | 37 | #So this is weird if only one file is there it doesn't have a count property! Probably better to use measure-object 38 | try { 39 | $mailboxOst.count | Out-Null 40 | $count = $mailboxOst.count 41 | } 42 | catch { 43 | $count = 1 44 | } 45 | #Write-Log "Found $count ost files for $mailbox" 46 | 47 | if ($count -gt 1) { 48 | 49 | $ostDelNum = $count - 1 50 | #Write-Log "Deleting $ostDelNum ost files" 51 | try { 52 | $latestOst = $mailboxOst | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1 53 | $mailboxOst | Where-Object {$_.Name -ne $latestOst.Name} | Remove-Item -Force -ErrorAction Stop 54 | } 55 | catch { 56 | #write-log -level Error "Failed to delete ost files in $vhd for $mailbox" 57 | } 58 | } 59 | else { 60 | #Write-Log "Only One ost file found for $mailbox. No action taken" 61 | $ostDelNum = 0 62 | } 63 | 64 | } 65 | } 66 | } 67 | } #Process 68 | END {} #End 69 | } #function Remove-FslMultiOst 70 | 71 | # to parmaterise 72 | $userName = 'jim' 73 | $O365Folder = "\\labdc01\FSLogixContainers\" 74 | $vhdx = $false 75 | 76 | if ($vhdx) { 77 | $extension = '.vhdx' 78 | } 79 | else { 80 | $extension = '.vhd' 81 | } 82 | 83 | # Get the sid for the path 84 | try { 85 | $sid = Get-ADUser -Identity $userName -ErrorAction Stop | Select-Object -ExpandProperty SID 86 | } 87 | catch { 88 | Write-Error "SID Not Found for $userName" 89 | exit 90 | } 91 | 92 | # todo: take account of naming conventions 93 | $containingFolder = $userName + '_' + $sid 94 | 95 | $vhdName = 'ODFC_' + $userName + $extension 96 | 97 | $vhdPath = Join-Path $O365Folder (Join-Path $containingFolder $vhdName) 98 | 99 | #are the prereqs present? 100 | if ( -not (Test-Path $vhdPath)) { 101 | Write-Error 'VHD Not Found' 102 | exit 103 | } 104 | 105 | if (Test-Path HKLM:\SOFTWARE\FSLogix\Apps) { 106 | $InstallPath = (Get-ItemProperty HKLM:\SOFTWARE\FSLogix\Apps).InstallPath 107 | } 108 | else { 109 | Write-Error 'Install Not Found' 110 | exit 111 | } 112 | 113 | $frxPath = Join-Path $InstallPath frx.exe 114 | if ( -not (Test-Path $frxPath )) { 115 | Write-Error 'frx.exe Not Found' 116 | exit 117 | } 118 | 119 | #mount vhd 120 | try { 121 | $mountedDisk = Mount-DiskImage -ImagePath $vhdPath -NoDriveLetter -PassThru -ErrorAction Stop | Get-DiskImage -ErrorAction Stop 122 | } 123 | catch { 124 | Write-Error 'Failed to mount disk' 125 | exit 126 | } 127 | 128 | #Assign vhd to a random path in temp 129 | $tempGUID = New-Guid 130 | $mountPath = Join-Path $Env:Temp 131 | 132 | try { 133 | New-Item -Path $mountPath -ItemType Directory -ErrorAction Stop | Out-Null 134 | } 135 | catch { 136 | Write-Error 'Failed to create mounting directory' 137 | exit 138 | } 139 | 140 | try { 141 | Add-PartitionAccessPath -DiskNumber $mountedDisk.Number -PartitionNumber 1 -AccessPath $mountPath -ErrorAction Stop 142 | } 143 | catch { 144 | Write-Error 'Failed to create junction point' 145 | exit 146 | } 147 | 148 | # Now we have a path, remove dupe osts 149 | Remove-FslMultiOst -Path (Join-Path $mountPath 'ODFC') -ErrorAction Stop 150 | 151 | #copy the vhd 152 | #todo: rename old vhd before mounting and copy contents to new of same name? create temp first, then 153 | $newVHDName = Join-Path $Env:Temp ($tempGUID + $extension) 154 | 155 | $label = 'O365-' + $userName 156 | 157 | #copy contents 158 | $argumentList = "copyto-vhd -filename=$newVHDName -src=$mountPath -dynamic=1 -label=$label" 159 | 160 | Start-Process -FilePath $frxPath -ArgumentList $argumentList -Wait -NoNewWindow 161 | 162 | #Clean up 163 | try { 164 | Remove-PartitionAccessPath -DiskNumber $mountedDisk.Number -PartitionNumber 1 -AccessPath $mountPath -ErrorAction Stop 165 | } 166 | catch { 167 | Write-Warning 'Failed to remove junction point' 168 | } 169 | 170 | try { 171 | $mountedDisk | Dismount-DiskImage -ErrorAction Stop 172 | } 173 | catch { 174 | Write-Warning 'Failed to dismount disk' 175 | } 176 | 177 | try { 178 | Remove-Item -Path $mountPath -ErrorAction Stop 179 | } 180 | catch { 181 | Write-Warning "Failed to delete temp mount directory $mountPath" 182 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 FSLogix, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Mount-FslDisk/Dismount-FslDisk.ps1: -------------------------------------------------------------------------------- 1 | function Dismount-FslDisk { 2 | [CmdletBinding()] 3 | 4 | Param ( 5 | [Parameter( 6 | Position = 1, 7 | ValuefromPipelineByPropertyName = $true, 8 | ValuefromPipeline = $true, 9 | Mandatory = $true 10 | )] 11 | [String]$Path, 12 | 13 | [Parameter( 14 | ValuefromPipelineByPropertyName = $true, 15 | ValuefromPipeline = $true, 16 | Mandatory = $true 17 | )] 18 | [int16]$DiskNumber, 19 | 20 | [Parameter( 21 | ValuefromPipelineByPropertyName = $true, 22 | Mandatory = $true 23 | )] 24 | [String]$ImagePath, 25 | 26 | [Parameter( 27 | ValuefromPipelineByPropertyName = $true 28 | )] 29 | [Switch]$PassThru 30 | ) 31 | 32 | BEGIN { 33 | Set-StrictMode -Version Latest 34 | } # Begin 35 | PROCESS { 36 | 37 | # FSLogix Disk Partition Number this won't work with vhds created with MS tools as their main partition number is 2 38 | $partitionNumber = 1 39 | 40 | if ($PassThru) { 41 | $junctionPointRemoved = $false 42 | $mountRemoved = $false 43 | $directoryRemoved = $false 44 | } 45 | 46 | # Reverse the three tasks from Mount-FslDisk 47 | try { 48 | Remove-PartitionAccessPath -DiskNumber $DiskNumber -PartitionNumber $partitionNumber -AccessPath $Path -ErrorAction Stop | Out-Null 49 | $junctionPointRemoved = $true 50 | } 51 | catch { 52 | Write-Error "Failed to remove the junction point to $Path" 53 | } 54 | 55 | try { 56 | Dismount-DiskImage -ImagePath $ImagePath -ErrorAction Stop | Out-Null 57 | $mountRemoved = $true 58 | } 59 | catch { 60 | Write-Error "Failed to dismount disk $ImagePath" 61 | } 62 | 63 | try { 64 | Remove-Item -Path $Path -ErrorAction Stop | Out-Null 65 | $directoryRemoved = $true 66 | } 67 | catch { 68 | Write-Error "Failed to delete temp mount directory $Path" 69 | } 70 | 71 | If ($PassThru) { 72 | $output = [PSCustomObject]@{ 73 | JunctionPointRemoved = $junctionPointRemoved 74 | MountRemoved = $mountRemoved 75 | DirectoryRemoved = $directoryRemoved 76 | } 77 | Write-Output $output 78 | } 79 | Write-Verbose "Dismounted $ImagePath" 80 | } #Process 81 | END {} #End 82 | } #function Dismount-FslDisk -------------------------------------------------------------------------------- /Mount-FslDisk/Mount-FslDisk.ps1: -------------------------------------------------------------------------------- 1 | function Mount-FslDisk { 2 | [CmdletBinding()] 3 | 4 | Param ( 5 | [Parameter( 6 | Position = 1, 7 | ValuefromPipelineByPropertyName = $true, 8 | ValuefromPipeline = $true, 9 | Mandatory = $true 10 | )] 11 | [alias('FullName')] 12 | [System.String]$Path, 13 | 14 | [Parameter( 15 | ValuefromPipelineByPropertyName = $true 16 | )] 17 | [Switch]$PassThru 18 | ) 19 | 20 | BEGIN { 21 | Set-StrictMode -Version Latest 22 | } # Begin 23 | PROCESS { 24 | 25 | # FSLogix Disk Partition Number this won't work with vhds created with MS tools as their main partition number is 2 26 | $partitionNumber = 1 27 | 28 | try { 29 | # Mount the disk without a drive letter and get it's info, Mount-DiskImage is used to remove reliance on Hyper-V tools 30 | $mountedDisk = Mount-DiskImage -ImagePath $Path -NoDriveLetter -PassThru -ErrorAction Stop | Get-DiskImage -ErrorAction Stop 31 | } 32 | catch { 33 | Write-Error "Failed to mount disk $Path" 34 | return 35 | } 36 | 37 | # Assign vhd to a random path in temp folder so we don't have to worry about free drive letters which can be horrible 38 | # New-Guid not used here for PoSh 3 compatibility 39 | $tempGUID = [guid]::NewGuid().ToString() 40 | $mountPath = Join-Path $Env:Temp ('FSLogixMnt-' + $tempGUID) 41 | 42 | try { 43 | # Create directory which we will mount too 44 | New-Item -Path $mountPath -ItemType Directory -ErrorAction Stop | Out-Null 45 | } 46 | catch { 47 | Write-Error "Failed to create mounting directory $mountPath" 48 | # Cleanup 49 | $mountedDisk | Dismount-DiskImage -ErrorAction SilentlyContinue 50 | return 51 | } 52 | 53 | try { 54 | 55 | $addPartitionAccessPathParams = @{ 56 | DiskNumber = $mountedDisk.Number 57 | PartitionNumber = $partitionNumber 58 | AccessPath = $mountPath 59 | ErrorAction = 'Stop' 60 | } 61 | 62 | Add-PartitionAccessPath @addPartitionAccessPathParams 63 | } 64 | catch { 65 | Write-Error "Failed to create junction point to $mountPath" 66 | # Cleanup 67 | Remove-Item -Path $mountPath -ErrorAction SilentlyContinue 68 | $mountedDisk | Dismount-DiskImage -ErrorAction SilentlyContinue 69 | return 70 | } 71 | 72 | if ($PassThru) { 73 | # Create output required for piping to Dismount-FslDisk 74 | $output = [PSCustomObject]@{ 75 | Path = $mountPath 76 | DiskNumber = $mountedDisk.Number 77 | ImagePath = $mountedDisk.ImagePath 78 | } 79 | Write-Output $output 80 | } 81 | Write-Verbose "Mounted $Path" 82 | } #Process 83 | END { 84 | 85 | } #End 86 | } #function Mount-FslDisk 87 | -------------------------------------------------------------------------------- /Mount-FslDisk/Tests/Dismount-FslDisk.Tests.ps1: -------------------------------------------------------------------------------- 1 | $here = Split-Path -Parent $MyInvocation.MyCommand.Path | Split-Path -Parent 2 | $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.' 3 | . $here\$sut 4 | 5 | Describe "Testing $($sut.trimend('.ps1'))" { 6 | Mock -CommandName Remove-PartitionAccessPath -MockWith { $null } 7 | Mock -CommandName Dismount-DiskImage -MockWith { $null } 8 | Mock -CommandName Remove-Item -MockWith { $null } 9 | 10 | $path = "$env:temp\guid" 11 | $dn = 2 12 | $im = "Testdrive:\FakeDisk.vhdx" 13 | 14 | Context 'Input' { 15 | It 'Testing named parameters' { 16 | $result = Dismount-FslDisk -Path $path -DiskNumber $dn -ImagePath $im -PassThru 17 | $result.directoryRemoved | Should -BeTrue 18 | } 19 | It 'Test Positional Params' { 20 | $result = Dismount-FslDisk $path -DiskNumber $dn -ImagePath $im -PassThru 21 | $result.directoryRemoved | Should -BeTrue 22 | } 23 | It 'Test Pipe by value String Params' { 24 | $result = $path | Dismount-FslDisk -DiskNumber $dn -ImagePath $im -PassThru 25 | $result.directoryRemoved | Should -BeTrue 26 | } 27 | It 'Test Pipe by value Int Params' { 28 | $result = $dn | Dismount-FslDisk $path -ImagePath $im -PassThru 29 | $result.directoryRemoved | Should -BeTrue 30 | } 31 | It 'Pipe by property name' { 32 | $pipe = [PSCustomObject]@{ 33 | Path = $path 34 | DiskNumber = $dn 35 | ImagePath = $im 36 | Passthru = $true 37 | } 38 | 39 | $result = $pipe | Dismount-FslDisk 40 | $result.directoryRemoved | Should -BeTrue 41 | } 42 | 43 | } 44 | 45 | Context 'Execution' { 46 | Mock -CommandName Remove-PartitionAccessPath -MockWith { Throw 'Pester Error' } 47 | } 48 | 49 | Context 'Output'{ 50 | It 'Gives no output when Passthru not stated' { 51 | $result = Dismount-FslDisk -Path $path -DiskNumber $dn -ImagePath $im 52 | $result | Should -BeNullOrEmpty 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /Mount-FslDisk/Tests/Mount-FslDisk.Tests.ps1: -------------------------------------------------------------------------------- 1 | $here = Split-Path -Parent $MyInvocation.MyCommand.Path | Split-Path -Parent 2 | $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.' 3 | . $here\$sut 4 | 5 | Describe "Testing $($sut.trimend('.ps1'))" { 6 | 7 | $fakeDisk = 'TestDrive:\MadeUp.vhdx' 8 | 9 | Mock -CommandName Mount-DiskImage -MockWith { 10 | [PSCustomObject]@{ 11 | ImagePath = $fakeDisk 12 | } 13 | } 14 | Mock -CommandName Get-DiskImage -MockWith { 15 | [PSCustomObject]@{ 16 | Number = 3 17 | ImagePath = $fakeDisk 18 | } 19 | } 20 | Mock -CommandName New-Item -MockWith { $null } 21 | Mock -CommandName Add-PartitionAccessPath -MockWith { $null } 22 | Mock -CommandName Remove-Item -MockWith { $null } 23 | Mock -CommandName Dismount-DiskImage -MockWith { $null } 24 | 25 | Context "Input" { 26 | 27 | It 'Runs with Named Parameter' { 28 | $result = Mount-FslDisk -Path $fakeDisk -PassThru 29 | $result.Path | Should -not -BeNullOrEmpty 30 | } 31 | 32 | It 'Runs with Positional Parameter' { 33 | $result = Mount-FslDisk $fakeDisk -PassThru 34 | $result.Path | Should -not -BeNullOrEmpty 35 | } 36 | 37 | It 'Runs with Pipeline by value' { 38 | $result = $fakeDisk | Mount-FslDisk -PassThru 39 | $result.Path | Should -not -BeNullOrEmpty 40 | } 41 | 42 | It 'Runs with Pipeline by named value' { 43 | $pipe = [PSCustomObject]@{ 44 | ImagePath = $fakeDisk 45 | } 46 | $result = $pipe | Mount-FslDisk -PassThru 47 | $result.Path | Should -not -BeNullOrEmpty 48 | } 49 | } 50 | 51 | Context 'Execution with no errors' { 52 | 53 | Mount-FslDisk -Path $fakeDisk 54 | 55 | It 'Does not call Remove-Item mock' { 56 | 57 | Assert-MockCalled -CommandName Remove-Item -Times 0 58 | } 59 | It 'Does not call Dismount-DiskImage mock' { 60 | Assert-MockCalled -CommandName Dismount-DiskImage -Times 0 61 | } 62 | It 'Calls New-Item mock Once' { 63 | Assert-MockCalled -CommandName New-Item -Times 1 64 | } 65 | It 'Calls Mount-DiskImage mock Once' { 66 | Assert-MockCalled -CommandName Mount-DiskImage -Times 1 67 | } 68 | It 'Calls Get-DiskImage mock Once' { 69 | Assert-MockCalled -CommandName Get-DiskImage -Times 1 70 | } 71 | It 'Calls Add-PartitionAccessPath mock Once' { 72 | Assert-MockCalled -CommandName Add-PartitionAccessPath -Times 1 73 | } 74 | } 75 | 76 | Context 'Execution with Mount Error' { 77 | 78 | Mock -CommandName Mount-DiskImage -MockWith { 79 | Throw 'pester mount error' 80 | } 81 | 82 | It 'Fails With correct error at mount' { 83 | Mount-FslDisk -Path $fakeDisk -ErrorVariable mnt -ErrorAction SilentlyContinue | Out-Null 84 | $mnt[-1].Exception.Message | Should -BeLike "Failed to mount disk*" 85 | } 86 | 87 | It 'Does not continue script' { 88 | Assert-MockCalled -CommandName Get-DiskImage -Times 0 89 | } 90 | } 91 | 92 | Context 'Execution with New-item Error' { 93 | 94 | Mock -CommandName New-Item -MockWith { 95 | Throw 'pester New-item error' 96 | } 97 | 98 | It 'Fails With correct error at New-item' { 99 | Mount-FslDisk -Path $fakeDisk -ErrorVariable itm -ErrorAction SilentlyContinue | Out-Null 100 | $itm[-1].Exception.Message | Should -BeLike "Failed to create mounting directory*" 101 | } 102 | 103 | It 'Does not continue script' { 104 | Assert-MockCalled -CommandName Add-PartitionAccessPath -Times 0 105 | } 106 | It 'Does Run Cleanup' { 107 | Assert-MockCalled -CommandName Dismount-DiskImage -Times 1 108 | } 109 | } 110 | 111 | Context 'Execution with Add-PartitionAccessPath Error' { 112 | 113 | Mock -CommandName Add-PartitionAccessPath -MockWith { 114 | Throw 'pester Add-PartitionAccessPath error' 115 | } 116 | 117 | Mock -CommandName Write-Output -MockWith { 118 | 'Fake Output' 119 | } 120 | 121 | $prt = $null 122 | 123 | It 'Fails With correct error at Add-PartitionAccessPath' { 124 | Mount-FslDisk -Path $fakeDisk -ErrorVariable prt -ErrorAction SilentlyContinue | Out-Null 125 | $prt[-1].Exception.Message | Should -BeLike "Failed to create junction point to *" 126 | } 127 | 128 | It 'Does not continue script' { 129 | Assert-MockCalled -CommandName Write-Output -Times 0 130 | } 131 | It 'Does Run Mount Cleanup' { 132 | Assert-MockCalled -CommandName Dismount-DiskImage -Times 1 133 | } 134 | It 'Does Run Directory Cleanup' { 135 | Assert-MockCalled -CommandName Remove-Item -Times 1 136 | } 137 | } 138 | 139 | Context 'Output' { 140 | $result = Mount-FslDisk -Path 'fakedisk.vhdx' -PassThru 141 | 142 | It 'Gives no output without PassThru'{ 143 | $emptyResult = Mount-FslDisk -Path 'fakedisk.vhdx' 144 | $emptyResult | Should -BeNullOrEmpty 145 | } 146 | 147 | It 'Has three properties' { 148 | $result | Get-Member -MemberType NoteProperty | Should -HaveCount 3 149 | } 150 | 151 | It 'Has the correct ImagePath' { 152 | $result.ImagePath | Should -Be 'TestDrive:\MadeUp.vhdx' 153 | } 154 | 155 | It 'Has the correct DiskNumber' { 156 | $result.DiskNumber | Should -Be 3 157 | } 158 | 159 | It 'Has the correct Prefix in Path' { 160 | $result.Path | Split-Path -Leaf | Should -BeLike "FSLogixMnt-*" 161 | } 162 | 163 | It 'Has a GUID in Path' { 164 | $guid = ($result.Path | Split-Path -Leaf).TrimStart('FSLogixMnt-') 165 | try { 166 | [guid]$guid 167 | $test = $true 168 | } 169 | catch { 170 | $test = $false 171 | } 172 | $test | Should -BeTrue 173 | } 174 | } 175 | } -------------------------------------------------------------------------------- /Mount-FslDisk/Tests/createDiskForTest.ps1: -------------------------------------------------------------------------------- 1 | $frx = Join-Path $env:ProgramFiles 'FSLogix\Apps\frx.exe' 2 | 3 | $params = "create-vhd -filename $(Join-Path $env:TEMP test.vhdx)" 4 | 5 | Start-Process -FilePath $frx -ArgumentList $params -------------------------------------------------------------------------------- /Move-FslOutlookFolder/MoveFslOutlookFolder.ps1: -------------------------------------------------------------------------------- 1 | function Move-FslOulookFolder { 2 | [CmdletBinding(PositionalBinding = $true)] 3 | 4 | Param ( 5 | [Parameter( 6 | ValuefromPipelineByPropertyName = $true, 7 | ValuefromPipeline = $true, 8 | Mandatory = $true 9 | )] 10 | [System.String[]]$User, 11 | 12 | [Parameter( 13 | ValuefromPipelineByPropertyName = $true, 14 | Mandatory = $true 15 | )] 16 | [System.String]$ProfilePath, 17 | 18 | [Parameter( 19 | ValuefromPipelineByPropertyName = $true, 20 | Mandatory = $true 21 | )] 22 | [System.String]$O365Path, 23 | 24 | [Parameter( 25 | ValuefromPipelineByPropertyName = $true 26 | )] 27 | [System.String]$LogPath = "$Env:TEMP\Move-FslOulookFolder.log" 28 | ) 29 | 30 | BEGIN { 31 | Set-StrictMode -Version Latest 32 | #Requires -Modules "Hyper-V" 33 | #Requires -Modules "ActiveDirectory" 34 | #Requires -RunAsAdministrator 35 | 36 | $PSDefaultParameterValues = @{ 37 | "Write-Log:Path" = "$LogPath" 38 | "Write-Log:Verbose" = $false 39 | } 40 | #Write-Log -StartNew 41 | } # Begin 42 | PROCESS { 43 | 44 | foreach ($account in $User) { 45 | 46 | #Need the SID for the path 47 | try { 48 | $accountSID = Get-ADUser -Identity $account -ErrorAction Stop | Select-Object -ExpandProperty SID 49 | } 50 | catch { 51 | $error[0] | Write-Log 52 | Write-Log -Level Error -Message "Cannot find SID for $account, Stopping processing" 53 | break 54 | } 55 | 56 | #Create Flip/Flop paths, won't work if customer has default path values or vhd disk files 57 | try{ 58 | $profileVHDPath = Join-Path -Path $ProfilePath -ChildPath (Join-Path -Path $account + '_' + $accountSID -ChildPath 'Profile_' + $account + '.vhdx' -ErrorAction Stop) -ErrorAction Stop 59 | 60 | $o365VHDPath = Join-Path -Path $O365Path -ChildPath (Join-Path -Path $account + '_' + $accountSID -ChildPath 'ODFC_' + $account + '.vhdx' -ErrorAction Stop) -ErrorAction Stop 61 | } 62 | catch{ 63 | $error[0] | Write-Log 64 | Write-Log -Level Error -Message "Cannot create vhd location paths for $account, Stopping processing" 65 | break 66 | } 67 | 68 | #make sure vhdxs really exist 69 | if (-not (Test-Path -Path $profileVHDPath)){ 70 | Write-Log -Level Error -Message "Cannot find $profileVHDPath, Stopping processing $account" 71 | break 72 | } 73 | 74 | if (-not (Test-Path -Path $o365VHDPath)){ 75 | Write-Log -Level Error -Message "Cannot find $o365VHDPath, Stopping processing $account" 76 | break 77 | } 78 | 79 | #get last 2 free drive letters 80 | $freeDrives = [char[]](68..90) | Where-Object { -not (Test-Path ($_ + ':')) } | Select-Object -Last 2 81 | 82 | if (($freeDrives | Measure-Object | Select-Object -ExpandProperty count) -lt 2){ 83 | Write-Log -Level Warning 'Not enough free drive letters, trying to free spare' 84 | Write-Warning 'Not enough free drive letters, trying to free spare letters' 85 | get-disk | Where-Object {$_.FriendlyName -eq "Msft Virtual Disk"} | Select-Object -ExpandProperty location | Dismount-DiskImage 86 | $freeDrives = [char[]](68..90) | Where-Object { -not (Test-Path ($_ + ':')) } | Select-Object -last 2 87 | } 88 | 89 | #Mount both disks and assign a free drive letter 90 | try{ 91 | Mount-VHD -Path $profileVHDPath -NoDriveLetter -Passthru -ErrorAction Stop | Get-Disk | Get-Partition | Where-Object { $_.type -eq 'Basic' } | Set-Partition -NewDriveLetter $freeDrives[0] 92 | } 93 | catch{ 94 | $error[0] | Write-Log 95 | Write-Log -Level Error -Message "Cannot Mount profile disk for $account, Stopping processing" 96 | break 97 | } 98 | 99 | try{ 100 | Mount-VHD -Path $o365VHDPath -NoDriveLetter -Passthru -ErrorAction Stop | Get-Disk | Get-Partition | Where-Object { $_.type -eq 'Basic' } | Set-Partition -NewDriveLetter $freeDrives[1] 101 | } 102 | catch{ 103 | $error[0] | Write-Log 104 | Write-Log -Level Error -Message "Cannot mount o365 disk for $account, Stopping processing" 105 | break 106 | } 107 | 108 | #Move Oulook folder 109 | try { 110 | Move-Item ( Join-Path -Path $freeDrives[0] -ChildPath 'ODFC\Outlook' ) -Destination ( Join-Path -Path $freeDrives[1] -ChildPath 'ODFC\Outlook' ) -Force 111 | } 112 | catch { 113 | $error[0] | Write-Log 114 | Write-Log -Level Error -Message "Cannot move Outlook folder for $account, Stopping processing" 115 | break 116 | } 117 | 118 | try{ 119 | DisMount-VHD -Path $o365VHDPath -ErrorAction Stop 120 | } 121 | catch{ 122 | $error[0] | Write-Log 123 | Write-Log -Level Warning -Message "Cannot DisMount o365 disk for $account" 124 | Write-Warning "Cannot DisMount o365 disk for $account" 125 | } 126 | 127 | try{ 128 | DisMount-VHD -Path $profileVHDPath -ErrorAction Stop 129 | } 130 | catch{ 131 | $error[0] | Write-Log 132 | Write-Log -Level Warning -Message "Cannot DisMount profile disk for $account" 133 | Write-Warning "Cannot DisMount profile disk for $account" 134 | } 135 | Write-Log "Done $account" 136 | } 137 | } #Process 138 | END {} #End 139 | } #function Move-FslOulookFolder -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Miscellaneous-Scripts 2 | Scripts which don't yet/Might never warrant their own repo 3 | 4 | ## Rename-FslDisk 5 | Renames disk based on regex matches and logs the result. 6 | 7 | ## Resize-FslDisk 8 | Function to resize an FsLogix disk 9 | -------------------------------------------------------------------------------- /Rename-FslDisk/Functions/Rename-FslDisk.ps1: -------------------------------------------------------------------------------- 1 | function Rename-FslDisk { 2 | [CmdletBinding()] 3 | 4 | Param ( 5 | [Parameter( 6 | ParameterSetName = 'File', 7 | Position = 0, 8 | ValuefromPipelineByPropertyName = $true, 9 | ValuefromPipeline = $true, 10 | Mandatory = $true 11 | )] 12 | [System.String]$PathToDisk, 13 | 14 | [Parameter( 15 | ParameterSetName = 'Folder', 16 | Position = 0, 17 | ValuefromPipelineByPropertyName = $true, 18 | Mandatory = $true 19 | )] 20 | [System.String]$Folder, 21 | 22 | [Parameter( 23 | Position = 0, 24 | ValuefromPipelineByPropertyName = $true, 25 | ValuefromPipeline = $true 26 | )] 27 | [regex]$OriginalMatch = "^(.*?)_S-\d-\d+-(\d+-){1,14}\d+$", 28 | 29 | [Parameter( 30 | Position = 0, 31 | ValuefromPipelineByPropertyName = $true 32 | )] 33 | [string]$LogPath = "$env:TEMP\Rename-FslDisk.log" 34 | 35 | ) 36 | 37 | BEGIN { 38 | Set-StrictMode -Version Latest 39 | #Write-Log 40 | #Rename-SingleDisk 41 | $PSDefaultParameterValues = @{"Write-Log:Path" = "$LogPath"} 42 | Write-Log -StartNew 43 | } # Begin 44 | PROCESS { 45 | switch ($PSCmdlet.ParameterSetName) { 46 | Folder { 47 | $files = Get-ChildItem -Path $Folder -Recurse -File -Filter *.vhd* 48 | if ($files.count -eq 0){ 49 | Write-Error "No files found in location $Folder" 50 | Write-Log -Level Error "No files found in location $Folder" 51 | } 52 | } 53 | File { 54 | $files = foreach ($disk in $PathToDisk){ 55 | if (Test-Path $disk){ 56 | Get-ChildItem -Path $disk 57 | } 58 | else{ 59 | Write-Error "$disk does not exist" 60 | Write-Log -Level Error "$disk does not exist" 61 | } 62 | } 63 | } 64 | } #switch 65 | 66 | foreach ($file in $files){ 67 | if ($file.BaseName -match $OriginalMatch){ 68 | $newName = "Profile_$($Matches[1])$($file.Extension)" 69 | Rename-SingleDisk -Path $file.FullName -NewName $newName -LogPath $LogPath 70 | } 71 | else{ 72 | Write-Log -Level Warn "$file does not match regex" 73 | } 74 | } 75 | 76 | } #Process 77 | END {} #End 78 | } #function Rename-FslDisk -------------------------------------------------------------------------------- /Rename-FslDisk/Functions/Rename-SingleDisk.ps1: -------------------------------------------------------------------------------- 1 | function Rename-SingleDisk { 2 | [CmdletBinding()] 3 | 4 | Param ( 5 | [Parameter( 6 | Position = 0, 7 | ValuefromPipelineByPropertyName = $true, 8 | ValuefromPipeline = $true, 9 | Mandatory = $true 10 | )] 11 | [System.String]$Path, 12 | 13 | [Parameter( 14 | Position = 0, 15 | ValuefromPipelineByPropertyName = $true, 16 | Mandatory = $true 17 | )] 18 | [System.String]$NewName, 19 | 20 | [Parameter( 21 | Position = 0, 22 | ValuefromPipelineByPropertyName = $true, 23 | Mandatory = $true 24 | )] 25 | [string]$LogPath 26 | ) 27 | 28 | BEGIN { 29 | Set-StrictMode -Version Latest 30 | $PSDefaultParameterValues = @{"Write-Log:Path" = "$LogPath"} 31 | } # Begin 32 | PROCESS { 33 | try{ 34 | Rename-Item -Path $Path -NewName $NewName -ErrorAction Stop 35 | Write-Log "Renamed $Path to $NewName" 36 | Write-Verbose "Renamed $Path to $NewName" 37 | } 38 | catch{ 39 | Write-Log -Level Error "Failed to rename $Path" 40 | } 41 | } #Process 42 | END {} #End 43 | } #function Rename-SingleDisk -------------------------------------------------------------------------------- /Rename-FslDisk/Release/Rename-FslDisk.ps1: -------------------------------------------------------------------------------- 1 | function Rename-FslDisk { 2 | [CmdletBinding()] 3 | 4 | Param ( 5 | [Parameter( 6 | ParameterSetName = 'File', 7 | Position = 0, 8 | ValuefromPipelineByPropertyName = $true, 9 | ValuefromPipeline = $true, 10 | Mandatory = $true 11 | )] 12 | [System.String]$PathToDisk, 13 | 14 | [Parameter( 15 | ParameterSetName = 'Folder', 16 | Position = 0, 17 | ValuefromPipelineByPropertyName = $true, 18 | Mandatory = $true 19 | )] 20 | [System.String]$Folder, 21 | 22 | [Parameter( 23 | Position = 0, 24 | ValuefromPipelineByPropertyName = $true, 25 | ValuefromPipeline = $true 26 | )] 27 | [regex]$OriginalMatch = "^(.*?)_S-\d-\d+-(\d+-){1,14}\d+$", 28 | 29 | [Parameter( 30 | Position = 0, 31 | ValuefromPipelineByPropertyName = $true 32 | )] 33 | [string]$LogPath = "$env:TEMP\Rename-FslDisk.log" 34 | 35 | ) 36 | 37 | BEGIN { 38 | Set-StrictMode -Version Latest 39 | #Write-Log 40 | function Write-Log { 41 | [CmdletBinding(DefaultParametersetName = "LOG")] 42 | Param ( 43 | [Parameter(Mandatory = $true, 44 | ValueFromPipelineByPropertyName = $true, 45 | Position = 0, 46 | ParameterSetName = 'LOG')] 47 | [ValidateNotNullOrEmpty()] 48 | [string]$Message, 49 | 50 | [Parameter(Mandatory = $false, 51 | Position = 1, 52 | ParameterSetName = 'LOG')] 53 | [ValidateSet("Error", "Warn", "Info")] 54 | [string]$Level = "Info", 55 | 56 | [Parameter(Mandatory = $false, 57 | Position = 2)] 58 | [string]$Path = "$env:temp\PowershellScript.log", 59 | 60 | [Parameter(Mandatory = $false, 61 | Position = 3, 62 | ParameterSetName = 'STARTNEW')] 63 | [switch]$StartNew, 64 | 65 | [Parameter(Mandatory = $false, 66 | Position = 4, 67 | ValueFromPipeline = $true, 68 | ValueFromPipelineByPropertyName = $true, 69 | ParameterSetName = 'EXCEPTION')] 70 | [System.Management.Automation.ErrorRecord]$Exception 71 | 72 | ) 73 | 74 | BEGIN { 75 | Set-StrictMode -version Latest 76 | $expandedParams = $null 77 | $PSBoundParameters.GetEnumerator() | ForEach-Object { $expandedParams += ' -' + $_.key + ' '; $expandedParams += $_.value } 78 | Write-Verbose "Starting: $($MyInvocation.MyCommand.Name)$expandedParams" 79 | } 80 | PROCESS { 81 | 82 | switch ($PSCmdlet.ParameterSetName) { 83 | EXCEPTION { 84 | Write-Log -Level Error -Message $Exception.Exception.Message -Path $Path 85 | break 86 | } 87 | STARTNEW { 88 | Write-Verbose -Message "Deleting log file $Path if it exists" 89 | Remove-Item $Path -Force -ErrorAction SilentlyContinue 90 | Write-Verbose -Message 'Deleted log file if it exists' 91 | Write-Log 'Starting Logfile' -Path $Path 92 | break 93 | } 94 | LOG { 95 | Write-Verbose 'Getting Date for our Log File' 96 | $FormattedDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss" 97 | Write-Verbose 'Date is $FormattedDate' 98 | 99 | switch ( $Level ) { 100 | 'Error' { $LevelText = 'ERROR: '; break } 101 | 'Warn' { $LevelText = 'WARNING:'; break } 102 | 'Info' { $LevelText = 'INFO: '; break } 103 | } 104 | 105 | $logmessage = "$FormattedDate $LevelText $Message" 106 | Write-Verbose $logmessage 107 | 108 | $logmessage | Add-Content -Path $Path 109 | } 110 | } 111 | 112 | } 113 | END { 114 | Write-Verbose "Finished: $($MyInvocation.Mycommand)" 115 | } 116 | } # enable logging 117 | #Rename-SingleDisk 118 | function Rename-SingleDisk { 119 | [CmdletBinding()] 120 | 121 | Param ( 122 | [Parameter( 123 | Position = 0, 124 | ValuefromPipelineByPropertyName = $true, 125 | ValuefromPipeline = $true, 126 | Mandatory = $true 127 | )] 128 | [System.String]$Path, 129 | 130 | [Parameter( 131 | Position = 0, 132 | ValuefromPipelineByPropertyName = $true, 133 | Mandatory = $true 134 | )] 135 | [System.String]$NewName, 136 | 137 | [Parameter( 138 | Position = 0, 139 | ValuefromPipelineByPropertyName = $true, 140 | Mandatory = $true 141 | )] 142 | [string]$LogPath 143 | ) 144 | 145 | BEGIN { 146 | Set-StrictMode -Version Latest 147 | $PSDefaultParameterValues = @{"Write-Log:Path" = "$LogPath"} 148 | } # Begin 149 | PROCESS { 150 | try{ 151 | Rename-Item -Path $Path -NewName $NewName -ErrorAction Stop 152 | Write-Log "Renamed $Path to $NewName" 153 | Write-Verbose "Renamed $Path to $NewName" 154 | } 155 | catch{ 156 | Write-Log -Level Error "Failed to rename $Path" 157 | } 158 | } #Process 159 | END {} #End 160 | } #function Rename-SingleDisk 161 | $PSDefaultParameterValues = @{"Write-Log:Path" = "$LogPath"} 162 | Write-Log -StartNew 163 | } # Begin 164 | PROCESS { 165 | switch ($PSCmdlet.ParameterSetName) { 166 | Folder { 167 | $files = Get-ChildItem -Path $Folder -Recurse -File -Filter *.vhd* 168 | if ($files.count -eq 0){ 169 | Write-Error "No files found in location $Folder" 170 | Write-Log -Level Error "No files found in location $Folder" 171 | } 172 | } 173 | File { 174 | $files = foreach ($disk in $PathToDisk){ 175 | if (Test-Path $disk){ 176 | Get-ChildItem -Path $disk 177 | } 178 | else{ 179 | Write-Error "$disk does not exist" 180 | Write-Log -Level Error "$disk does not exist" 181 | } 182 | } 183 | } 184 | } #switch 185 | 186 | foreach ($file in $files){ 187 | if ($file.BaseName -match $OriginalMatch){ 188 | $newName = "Profile_$($Matches[1])$($file.Extension)" 189 | Rename-SingleDisk -Path $file -NewName $newName -LogPath $LogPath 190 | } 191 | else{ 192 | Write-Log -Level Warn "$file does not match regex" 193 | } 194 | } 195 | 196 | } #Process 197 | END {} #End 198 | } #function Rename-FslDisk 199 | -------------------------------------------------------------------------------- /Rename-FslDisk/build.ps1: -------------------------------------------------------------------------------- 1 | function Get-WriteLog { 2 | # --- Set the uri for the latest release 3 | $URI = "https://api.github.com/repos/JimMoyle/YetAnotherWriteLog/releases/latest" 4 | 5 | [Net.ServicePointManager]::SecurityProtocol = "tls12, tls11, tls" 6 | 7 | # --- Query the API to get the url of the zip 8 | $response = Invoke-RestMethod -Method Get -Uri $URI 9 | $zipUrl = $Response.zipball_url 10 | 11 | # --- Download the file to the current location 12 | $OutputPath = "$((Get-Location).Path)\$($Response.name.Replace(" ","_")).zip" 13 | Invoke-RestMethod -Method Get -Uri $ZipUrl -OutFile $OutputPath 14 | 15 | Expand-Archive -Path $OutputPath -DestinationPath $env:TEMP\zip\ -Force 16 | 17 | $writeLog = Get-ChildItem $env:TEMP\zip\ -Recurse -Include write-log.ps1 | Get-Content 18 | 19 | Write-Output $writeLog 20 | 21 | Remove-Item $OutputPath 22 | Remove-Item $env:TEMP\zip -Force -Recurse 23 | } 24 | 25 | function Add-FslReleaseFunction { 26 | [cmdletbinding()] 27 | param ( 28 | [Parameter( 29 | Position = 0, 30 | ValuefromPipelineByPropertyName = $true, 31 | Mandatory = $false 32 | )] 33 | [System.String]$FunctionsFolder = '.\Functions', 34 | 35 | [Parameter( 36 | Position = 1, 37 | ValuefromPipelineByPropertyName = $true, 38 | Mandatory = $false 39 | )] 40 | [System.String]$ReleaseFolder = '.\Release', 41 | [Parameter( 42 | Position = 1, 43 | ValuefromPipelineByPropertyName = $true, 44 | Mandatory = $true 45 | )] 46 | [System.String]$ControlScript 47 | ) 48 | 49 | $ctrlScript = Get-Content -Path (Join-Path $FunctionsFolder $ControlScript) 50 | 51 | if ($ctrlScript -match '#Write-Log') { 52 | $logger = Get-WriteLog 53 | $logger | Set-Content (Join-Path $FunctionsFolder Write-Log.ps1) 54 | } 55 | 56 | $funcs = Get-ChildItem $FunctionsFolder -File | Where-Object {$_.Name -ne $ControlScript} 57 | 58 | foreach ($funcName in $funcs) { 59 | 60 | $pattern = "#$($funcName.BaseName)" 61 | $actualFunc = Get-Content (Join-Path $FunctionsFolder $funcName) 62 | 63 | $ctrlScript = $ctrlScript | Foreach-Object { 64 | $_ 65 | if ($_ -match $pattern ) { 66 | $actualFunc 67 | } 68 | } 69 | } 70 | $ctrlScript | Set-Content (Join-Path $ReleaseFolder $ControlScript) 71 | } 72 | 73 | Add-FslReleaseFunction -ControlScript 'Rename-FslDisk.ps1' -------------------------------------------------------------------------------- /Resize-FslDisk/Functions/Resize-FslDisk.ps1: -------------------------------------------------------------------------------- 1 | function Resize-FslDisk { 2 | [CmdletBinding()] 3 | 4 | Param ( 5 | [Parameter( 6 | ParameterSetName = 'File', 7 | Position = 0, 8 | ValuefromPipelineByPropertyName = $true, 9 | ValuefromPipeline = $true, 10 | Mandatory = $true 11 | )] 12 | [System.String[]]$PathToDisk, 13 | 14 | [Parameter( 15 | ParameterSetName = 'Folder', 16 | Position = 0, 17 | ValuefromPipelineByPropertyName = $true, 18 | Mandatory = $true 19 | )] 20 | [System.String]$Folder, 21 | 22 | [Parameter( 23 | Position = 0, 24 | ValuefromPipelineByPropertyName = $true, 25 | ValuefromPipeline = $true, 26 | Mandatory = $true 27 | )] 28 | [uint64]$SizeBytes, 29 | 30 | <# 31 | [Parameter( 32 | Position = 0, 33 | ValuefromPipelineByPropertyName = $true 34 | )] 35 | [switch]$AsJob, 36 | #> 37 | 38 | [Parameter( 39 | Position = 0, 40 | ValuefromPipelineByPropertyName = $true 41 | )] 42 | [switch]$NewLog, 43 | 44 | 45 | [Parameter( 46 | Position = 0, 47 | ValuefromPipelineByPropertyName = $true 48 | )] 49 | [string]$LogPath = "$env:TEMP\Resize-FslDisk.log" 50 | ) 51 | 52 | BEGIN { 53 | Set-StrictMode -Version Latest 54 | #Write-Log.ps1 55 | $PSDefaultParameterValues = @{ 56 | "Write-Log:Path" = "$LogPath" 57 | "Write-Log:Verbose" = $false 58 | } 59 | 60 | if ($NewLog){ 61 | Write-Log -StartNew 62 | $NewLog = $false 63 | } 64 | 65 | if ((Get-Module -ListAvailable -Verbose:$false).Name -notcontains 'Hyper-V') { 66 | Write-Log -Level Error 'Hyper-V Powershell module not present' 67 | Write-Error 'Hyper-V Powershell module not present' 68 | exit 69 | } 70 | 71 | } # Begin 72 | PROCESS { 73 | switch ($PSCmdlet.ParameterSetName) { 74 | Folder { 75 | $vhds = Get-ChildItem -Path $Folder -Recurse -File -Filter *.vhd* 76 | if ($vhds.count -eq 0){ 77 | Write-Error "No vhd(x) files found in location $Folder" 78 | Write-Log -Level Error "No vhd(x) files found in location $Folder" 79 | } 80 | } 81 | File { 82 | $vhds = foreach ($disk in $PathToDisk){ 83 | if (Test-Path $disk){ 84 | Get-ChildItem -Path $disk 85 | } 86 | else{ 87 | Write-Error "$disk does not exist" 88 | Write-Log -Level Error "$disk does not exist" 89 | } 90 | } 91 | } 92 | } #switch 93 | 94 | foreach ($vhd in $vhds){ 95 | try{ 96 | $ResizeVHDParams = @{ 97 | #Passthru = $Passthru 98 | #AsJob = $AsJob 99 | SizeBytes = $SizeBytes 100 | ErrorAction = 'Stop' 101 | Path = $vhd.FullName 102 | } 103 | Resize-VHD @ResizeVHDParams 104 | } 105 | catch{ 106 | Write-Log -Level Error "$vhd has not been resized" 107 | } 108 | 109 | try { 110 | $mount = Mount-VHD $vhd -Passthru -ErrorAction Stop 111 | } 112 | catch { 113 | $Error[0] | Write-Log 114 | Write-Log -level Error "Failed to mount $vhd" 115 | Write-Log -level Error "Stopping processing $vhd" 116 | break 117 | } 118 | 119 | try{ 120 | $partitionNumber = 1 121 | $max = $mount | Get-PartitionSupportedSize -PartitionNumber $partitionNumber -ErrorAction Stop | Select-Object -ExpandProperty Sizemax 122 | $mount | Resize-Partition -size $max -PartitionNumber $partitionNumber -ErrorAction Stop 123 | } 124 | catch{ 125 | $Error[0] | Write-Log 126 | Write-Log -level Error "Failed to resize partition on $vhd" 127 | Write-Log -level Error "Stopping processing $vhd" 128 | break 129 | } 130 | 131 | try { 132 | Dismount-VHD $vhd -ErrorAction Stop 133 | Write-Verbose "$vhd has been resized to $SizeBytes Bytes" 134 | Write-Log "$vhd has been resized to $SizeBytes Bytes" 135 | } 136 | catch { 137 | $Error[0] | Write-Log 138 | write-log -level Error "Failed to Dismount $vhd vhd will need to be manually dismounted" 139 | } 140 | } 141 | } #Process 142 | END {} #End 143 | } #function Resize-FslDisk -------------------------------------------------------------------------------- /Resize-FslDisk/Functions/Tests/Resize-FslDisk.Tests.ps1: -------------------------------------------------------------------------------- 1 | $here = Split-Path -Parent $MyInvocation.MyCommand.Path 2 | $here = Split-Path $here 3 | $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.' 4 | . "$here\$sut" 5 | 6 | Describe 'Resize-FslDisk' { 7 | 8 | BeforeAll { 9 | . ..\Write-Log.ps1 10 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")] 11 | $size = 1073741824 12 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")] 13 | $path = 'testdrive:\test.vhd' 14 | } 15 | 16 | Mock -CommandName Resize-VHD -MockWith {} -Verifiable 17 | Mock -CommandName Test-Path -MockWith { $true } 18 | Mock -CommandName Get-ChildItem -MockWith { Write-Output @{ 19 | FullName = $path 20 | }} 21 | 22 | It 'Does not throw'{ 23 | { Resize-FslDisk -SizeBytes $size -Path $path } | should not throw 24 | } 25 | 26 | It 'Does not write Errors' { 27 | $errors = Resize-FslDisk -SizeBytes $size -Path $path 2>&1 28 | $errors.count | should Be 0 29 | } 30 | 31 | It 'Writes a Verbose line' { 32 | $verbose = Resize-FslDisk -SizeBytes $size -Path $path -Verbose 4>&1 33 | $verbose.count | should Be 1 34 | } 35 | 36 | It 'Asserts all verifiable mocks' { 37 | Assert-VerifiableMocks 38 | } 39 | 40 | It 'Takes pipeline input'{ 41 | 42 | } 43 | } -------------------------------------------------------------------------------- /Resize-FslDisk/Release/Resize-FslDisk.ps1: -------------------------------------------------------------------------------- 1 | function Resize-FslDisk { 2 | [CmdletBinding()] 3 | 4 | Param ( 5 | [Parameter( 6 | ParameterSetName = 'File', 7 | Position = 0, 8 | ValuefromPipelineByPropertyName = $true, 9 | ValuefromPipeline = $true, 10 | Mandatory = $true 11 | )] 12 | [System.String[]]$PathToDisk, 13 | 14 | [Parameter( 15 | ParameterSetName = 'Folder', 16 | Position = 0, 17 | ValuefromPipelineByPropertyName = $true, 18 | Mandatory = $true 19 | )] 20 | [System.String]$Folder, 21 | 22 | [Parameter( 23 | Position = 0, 24 | ValuefromPipelineByPropertyName = $true, 25 | ValuefromPipeline = $true, 26 | Mandatory = $true 27 | )] 28 | [uint64]$SizeBytes, 29 | 30 | <# 31 | [Parameter( 32 | Position = 0, 33 | ValuefromPipelineByPropertyName = $true 34 | )] 35 | [switch]$AsJob, 36 | #> 37 | 38 | [Parameter( 39 | Position = 0, 40 | ValuefromPipelineByPropertyName = $true 41 | )] 42 | [switch]$Passthru, 43 | 44 | [Parameter( 45 | Position = 0, 46 | ValuefromPipelineByPropertyName = $true 47 | )] 48 | [string]$LogPath = "$env:TEMP\Resize-FslDisk.log" 49 | ) 50 | 51 | BEGIN { 52 | Set-StrictMode -Version Latest 53 | 54 | function Write-Log { 55 | [CmdletBinding(DefaultParametersetName = "LOG")] 56 | Param ( 57 | [Parameter(Mandatory = $true, 58 | ValueFromPipelineByPropertyName = $true, 59 | Position = 0, 60 | ParameterSetName = 'LOG')] 61 | [ValidateNotNullOrEmpty()] 62 | [string]$Message, 63 | 64 | [Parameter(Mandatory = $false, 65 | Position = 1, 66 | ParameterSetName = 'LOG')] 67 | [ValidateSet("Error", "Warn", "Info")] 68 | [string]$Level = "Info", 69 | 70 | [Parameter(Mandatory = $false, 71 | Position = 2)] 72 | [string]$Path = "$env:temp\PowershellScript.log", 73 | 74 | [Parameter(Mandatory = $false, 75 | Position = 3, 76 | ParameterSetName = 'STARTNEW')] 77 | [switch]$StartNew, 78 | 79 | [Parameter(Mandatory = $false, 80 | Position = 4, 81 | ValueFromPipeline = $true, 82 | ValueFromPipelineByPropertyName = $true, 83 | ParameterSetName = 'EXCEPTION')] 84 | [System.Management.Automation.ErrorRecord]$Exception 85 | 86 | ) 87 | 88 | BEGIN { 89 | Set-StrictMode -version Latest 90 | $expandedParams = $null 91 | $PSBoundParameters.GetEnumerator() | ForEach-Object { $expandedParams += ' -' + $_.key + ' '; $expandedParams += $_.value } 92 | Write-Verbose "Starting: $($MyInvocation.MyCommand.Name)$expandedParams" 93 | } 94 | PROCESS { 95 | 96 | switch ($PSCmdlet.ParameterSetName) { 97 | EXCEPTION { 98 | Write-Log -Level Error -Message $Exception.Exception.Message -Path $Path 99 | break 100 | } 101 | STARTNEW { 102 | Write-Verbose -Message "Deleting log file $Path if it exists" 103 | Remove-Item $Path -Force -ErrorAction SilentlyContinue 104 | Write-Verbose -Message 'Deleted log file if it exists' 105 | Write-Log 'Starting Logfile' -Path $Path 106 | break 107 | } 108 | LOG { 109 | Write-Verbose 'Getting Date for our Log File' 110 | $FormattedDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss" 111 | Write-Verbose 'Date is $FormattedDate' 112 | 113 | switch ( $Level ) { 114 | 'Error' { $LevelText = 'ERROR: '; break } 115 | 'Warn' { $LevelText = 'WARNING:'; break } 116 | 'Info' { $LevelText = 'INFO: '; break } 117 | } 118 | 119 | $logmessage = "$FormattedDate $LevelText $Message" 120 | Write-Verbose $logmessage 121 | 122 | $logmessage | Add-Content -Path $Path 123 | } 124 | } 125 | 126 | } 127 | END { 128 | Write-Verbose "Finished: $($MyInvocation.Mycommand)" 129 | } 130 | } # enable logging 131 | 132 | $PSDefaultParameterValues = @{ 133 | "Write-Log:Path" = "$LogPath" 134 | "Write-Log:Verbose" = $false 135 | } 136 | Write-Log -StartNew 137 | if ((Get-Module -ListAvailable -Verbose:$false).Name -notcontains 'Hyper-V') { 138 | Write-Log -Level Error 'Hyper-V Powershell module not present' 139 | Write-Error 'Hyper-V Powershell module not present' 140 | exit 141 | } 142 | 143 | } # Begin 144 | PROCESS { 145 | switch ($PSCmdlet.ParameterSetName) { 146 | Folder { 147 | $vhds = Get-ChildItem -Path $Folder -Recurse -File -Filter *.vhd* 148 | if ($vhds.count -eq 0){ 149 | Write-Error "No vhd(x) files found in location $Folder" 150 | Write-Log -Level Error "No vhd(x) files found in location $Folder" 151 | } 152 | } 153 | File { 154 | $vhds = foreach ($disk in $PathToDisk){ 155 | if (Test-Path $disk){ 156 | Get-ChildItem -Path $disk 157 | } 158 | else{ 159 | Write-Error "$disk does not exist" 160 | Write-Log -Level Error "$disk does not exist" 161 | } 162 | } 163 | } 164 | } #switch 165 | 166 | foreach ($vhd in $vhds){ 167 | try{ 168 | $ResizeVHDParams = @{ 169 | Passthru = $Passthru 170 | #AsJob = $AsJob 171 | SizeBytes = $SizeBytes 172 | ErrorAction = 'Stop' 173 | Path = $vhd.FullName 174 | } 175 | Resize-VHD @ResizeVHDParams 176 | } 177 | catch{ 178 | Write-Log -Level Error "$vhd has not been resized" 179 | } 180 | 181 | try { 182 | $mount = Mount-VHD $vhd -Passthru -ErrorAction Stop 183 | } 184 | catch { 185 | $Error[0] | Write-Log 186 | Write-Log -level Error "Failed to mount $vhd" 187 | Write-Log -level Error "Stopping processing $vhd" 188 | break 189 | } 190 | 191 | try{ 192 | $partitionNumber = 2 193 | $max = $mount | Get-PartitionSupportedSize -PartitionNumber $partitionNumber -ErrorAction Stop | Select-Object -ExpandProperty Sizemax 194 | $mount | Resize-Partition -size $max -PartitionNumber $partitionNumber -ErrorAction Stop 195 | } 196 | catch{ 197 | $Error[0] | Write-Log 198 | Write-Log -level Error "Failed to resize partition on $vhd" 199 | Write-Log -level Error "Stopping processing $vhd" 200 | break 201 | } 202 | 203 | try { 204 | Dismount-VHD $vhd -ErrorAction Stop 205 | Write-Verbose "$vhd has been resized to $SizeBytes Bytes" 206 | Write-Log "$vhd has been resized to $SizeBytes Bytes" 207 | } 208 | catch { 209 | $Error[0] | Write-Log 210 | write-log -level Error "Failed to Dismount $vhd vhd will need to be manually dismounted" 211 | } 212 | } 213 | } #Process 214 | END {} #End 215 | } #function Resize-FslDisk 216 | -------------------------------------------------------------------------------- /Resize-FslDisk/build.ps1: -------------------------------------------------------------------------------- 1 | function Get-WriteLog { 2 | # --- Set the uri for the latest release 3 | $URI = "https://api.github.com/repos/JimMoyle/YetAnotherWriteLog/releases/latest" 4 | 5 | [Net.ServicePointManager]::SecurityProtocol = "tls12, tls11, tls" 6 | 7 | # --- Query the API to get the url of the zip 8 | $response = Invoke-RestMethod -Method Get -Uri $URI 9 | $zipUrl = $Response.zipball_url 10 | 11 | # --- Download the file to the current location 12 | $OutputPath = "$((Get-Location).Path)\$($Response.name.Replace(" ","_")).zip" 13 | Invoke-RestMethod -Method Get -Uri $ZipUrl -OutFile $OutputPath 14 | 15 | Expand-Archive -Path $OutputPath -DestinationPath $env:TEMP\zip\ -Force 16 | 17 | $writeLog = Get-ChildItem $env:TEMP\zip\ -Recurse -Include write-log.ps1 | Get-Content 18 | 19 | Write-Output $writeLog 20 | 21 | Remove-Item $OutputPath 22 | Remove-Item $env:TEMP\zip -Force -Recurse 23 | } 24 | 25 | function Add-FslRelease { 26 | [cmdletbinding()] 27 | param ( 28 | [Parameter( 29 | Position = 0, 30 | ValuefromPipelineByPropertyName = $true, 31 | Mandatory = $false 32 | )] 33 | [System.String]$FunctionsFolder = '.\Functions', 34 | 35 | [Parameter( 36 | Position = 1, 37 | ValuefromPipelineByPropertyName = $true, 38 | Mandatory = $false 39 | )] 40 | [System.String]$ReleaseFolder = '.\Release', 41 | [Parameter( 42 | Position = 1, 43 | ValuefromPipelineByPropertyName = $true, 44 | Mandatory = $true 45 | )] 46 | [System.String]$ControlScript 47 | ) 48 | 49 | $ctrlScript = Get-Content -Path (Join-Path $FunctionsFolder $ControlScript) 50 | 51 | if ($ctrlScript -match '#Write-Log') { 52 | $logger = Get-WriteLog 53 | $logger | Set-Content (Join-Path $FunctionsFolder Write-Log.ps1) 54 | } 55 | 56 | $funcs = Get-ChildItem $FunctionsFolder -File | Where-Object {$_.Name -ne $ControlScript} 57 | 58 | foreach ($funcName in $funcs) { 59 | 60 | $pattern = "#$($funcName.BaseName)" 61 | $actualFunc = Get-Content (Join-Path $FunctionsFolder $funcName) 62 | 63 | $ctrlScript = $ctrlScript | Foreach-Object { 64 | $_ 65 | if ($_ -match $pattern ) { 66 | $actualFunc 67 | } 68 | } 69 | } 70 | $ctrlScript | Set-Content (Join-Path $ReleaseFolder $ControlScript) 71 | } 72 | 73 | Add-FslRelease -ControlScript 'Resize-FslDisk.ps1' -------------------------------------------------------------------------------- /Shrink-FslDisk/Shrink-FslDisk.ps1: -------------------------------------------------------------------------------- 1 | function Invoke-FslShrinkDisk { 2 | [CmdletBinding()] 3 | 4 | Param ( 5 | 6 | [Parameter( 7 | Position = 1, 8 | ValuefromPipelineByPropertyName = $true, 9 | ValuefromPipeline = $true 10 | )] 11 | [System.String]$Path, 12 | 13 | [Parameter( 14 | ValuefromPipelineByPropertyName = $true 15 | )] 16 | [System.String]$IgnoreLessThanGB = 5, 17 | 18 | [Parameter( 19 | ValuefromPipelineByPropertyName = $true 20 | )] 21 | [System.String]$DeleteOlderThanDays, 22 | 23 | [Parameter( 24 | ValuefromPipelineByPropertyName = $true 25 | )] 26 | [Switch]$Recurse, 27 | 28 | [Parameter( 29 | ValuefromPipelineByPropertyName = $true 30 | )] 31 | [System.String]$LogFilePath 32 | ) 33 | 34 | BEGIN { 35 | Set-StrictMode -Version Latest 36 | #requires -Module Hyper-V 37 | #Write-Log 38 | function Write-Log { 39 | <# 40 | .SYNOPSIS 41 | 42 | Single function to enable logging to file. 43 | .DESCRIPTION 44 | 45 | The Log file can be output to any directory. A single log entry looks like this: 46 | 2018-01-30 14:40:35 INFO: 'My log text' 47 | 48 | Log entries can be Info, Warn, Error or Debug 49 | 50 | The function takes pipeline input and you can even pipe exceptions straight to the function for automatic logging. 51 | 52 | The $PSDefaultParameterValues built-in Variable can be used to conveniently set the path and/or JSONformat switch at the top of the script: 53 | 54 | $PSDefaultParameterValues = @{"Write-Log:Path" = 'C:\YourPathHere'} 55 | 56 | $PSDefaultParameterValues = @{"Write-Log:JSONformat" = $true} 57 | 58 | .PARAMETER Message 59 | 60 | This is the body of the log line and should contain the information you wish to log. 61 | .PARAMETER Level 62 | 63 | One of four logging levels: INFO, WARN, ERROR or DEBUG. This is an optional parameter and defaults to INFO 64 | .PARAMETER Path 65 | 66 | The path where you want the log file to be created. This is an optional parameter and defaults to "$env:temp\PowershellScript.log" 67 | .PARAMETER StartNew 68 | 69 | This will blank any current log in the path, it should be used at the start of a script when you don't want to append to an existing log. 70 | .PARAMETER Exception 71 | 72 | Used to pass a powershell exception to the logging function for automatic logging 73 | .PARAMETER JSONFormat 74 | 75 | Used to change the logging format from human readable to machine readable format, this will be a single line like the example format below: 76 | In this format the timestamp will include a much more granular time which will also include timezone information. 77 | 78 | {"TimeStamp":"2018-02-01T12:01:24.8908638+00:00","Level":"Warn","Message":"My message"} 79 | 80 | .EXAMPLE 81 | Write-Log -StartNew 82 | Starts a new logfile in the default location 83 | 84 | .EXAMPLE 85 | Write-Log -StartNew -Path c:\logs\new.log 86 | Starts a new logfile in the specified location 87 | 88 | .EXAMPLE 89 | Write-Log 'This is some information' 90 | Appends a new information line to the log. 91 | 92 | .EXAMPLE 93 | Write-Log -level warning 'This is a warning' 94 | Appends a new warning line to the log. 95 | 96 | .EXAMPLE 97 | Write-Log -level Error 'This is an Error' 98 | Appends a new Error line to the log. 99 | 100 | .EXAMPLE 101 | Write-Log -Exception $error[0] 102 | Appends a new Error line to the log with the message being the contents of the exception message. 103 | 104 | .EXAMPLE 105 | $error[0] | Write-Log 106 | Appends a new Error line to the log with the message being the contents of the exception message. 107 | 108 | .EXAMPLE 109 | 'My log message' | Write-Log 110 | Appends a new Info line to the log with the message being the contents of the string. 111 | 112 | .EXAMPLE 113 | Write-Log 'My log message' -JSONFormat 114 | Appends a new Info line to the log with the message. The line will be in JSONFormat. 115 | #> 116 | 117 | [CmdletBinding(DefaultParametersetName = "LOG")] 118 | Param ( 119 | [Parameter(Mandatory = $true, 120 | ValueFromPipeline = $true, 121 | ValueFromPipelineByPropertyName = $true, 122 | ParameterSetName = 'LOG', 123 | Position = 0)] 124 | [ValidateNotNullOrEmpty()] 125 | [string]$Message, 126 | 127 | [Parameter(Mandatory = $false, 128 | ValueFromPipelineByPropertyName = $true, 129 | ParameterSetName = 'LOG', 130 | Position = 1 )] 131 | [ValidateSet('Error', 'Warning', 'Info', 'Debug')] 132 | [string]$Level = "Info", 133 | 134 | [Parameter(Mandatory = $false, 135 | ValueFromPipelineByPropertyName = $true, 136 | Position = 2)] 137 | [string]$Path = "$env:temp\PowershellScript.log", 138 | 139 | [Parameter(Mandatory = $false, 140 | ValueFromPipelineByPropertyName = $true)] 141 | [switch]$JSONFormat, 142 | 143 | [Parameter(Mandatory = $false, 144 | ValueFromPipelineByPropertyName = $true, 145 | ParameterSetName = 'STARTNEW')] 146 | [switch]$StartNew, 147 | 148 | [Parameter(Mandatory = $true, 149 | ValueFromPipeline = $true, 150 | ValueFromPipelineByPropertyName = $true, 151 | ParameterSetName = 'EXCEPTION')] 152 | [System.Management.Automation.ErrorRecord]$Exception 153 | ) 154 | 155 | BEGIN { 156 | Set-StrictMode -version Latest #Enforces most strict best practice. 157 | } 158 | 159 | PROCESS { 160 | #Switch on parameter set 161 | switch ($PSCmdlet.ParameterSetName) { 162 | LOG { 163 | #Get human readable date 164 | $FormattedDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss" 165 | 166 | switch ( $Level ) { 167 | 'Info' { $LevelText = "INFO: "; break } 168 | 'Error' { $LevelText = "ERROR: "; break } 169 | 'Warning' { $LevelText = "WARNING:"; break } 170 | 'Debug' { $LevelText = "DEBUG: "; break } 171 | } 172 | 173 | if ($JSONFormat) { 174 | #Build an object so we can later convert it 175 | $logObject = [PSCustomObject]@{ 176 | TimeStamp = Get-Date -Format o #Get machine readable date 177 | Level = $Level 178 | Message = $Message 179 | } 180 | $logmessage = $logObject | ConvertTo-Json -Compress #Convert to a single line of JSON 181 | } 182 | else { 183 | $logmessage = "$FormattedDate $LevelText $Message" #Build human readable line 184 | } 185 | 186 | $logmessage | Add-Content -Path $Path #write the line to a file 187 | Write-Verbose $logmessage #Only verbose line in the function 188 | 189 | } #LOG 190 | 191 | EXCEPTION { 192 | #Splat parameters 193 | $WriteLogParams = @{ 194 | Level = 'Error' 195 | Message = $Exception.Exception.Message 196 | Path = $Path 197 | JSONFormat = $JSONFormat 198 | } 199 | Write-Log @WriteLogParams #Call itself to keep code clean 200 | break 201 | 202 | } #EXCEPTION 203 | 204 | STARTNEW { 205 | if (Test-Path $Path) { 206 | Remove-Item $Path -Force 207 | } 208 | #Splat parameters 209 | $WriteLogParams = @{ 210 | Level = 'Info' 211 | Message = 'Starting Logfile' 212 | Path = $Path 213 | JSONFormat = $JSONFormat 214 | } 215 | Write-Log @WriteLogParams 216 | break 217 | 218 | } #STARTNEW 219 | 220 | } #switch Parameter Set 221 | } 222 | 223 | END { 224 | } 225 | } #function 226 | #Invoke-Parallel 227 | function Invoke-Parallel { 228 | <# 229 | .SYNOPSIS 230 | Function to control parallel processing using runspaces 231 | 232 | .DESCRIPTION 233 | Function to control parallel processing using runspaces 234 | 235 | Note that each runspace will not have access to variables and commands loaded in your session or in other runspaces by default. 236 | This behaviour can be changed with parameters. 237 | 238 | .PARAMETER ScriptFile 239 | File to run against all input objects. Must include parameter to take in the input object, or use $args. Optionally, include parameter to take in parameter. Example: C:\script.ps1 240 | 241 | .PARAMETER ScriptBlock 242 | Scriptblock to run against all computers. 243 | 244 | You may use $Using: language in PowerShell 3 and later. 245 | 246 | The parameter block is added for you, allowing behaviour similar to foreach-object: 247 | Refer to the input object as $_. 248 | Refer to the parameter parameter as $parameter 249 | 250 | .PARAMETER InputObject 251 | Run script against these specified objects. 252 | 253 | .PARAMETER Parameter 254 | This object is passed to every script block. You can use it to pass information to the script block; for example, the path to a logging folder 255 | 256 | Reference this object as $parameter if using the scriptblock parameterset. 257 | 258 | .PARAMETER ImportVariables 259 | If specified, get user session variables and add them to the initial session state 260 | 261 | .PARAMETER ImportModules 262 | If specified, get loaded modules and pssnapins, add them to the initial session state 263 | 264 | .PARAMETER Throttle 265 | Maximum number of threads to run at a single time. 266 | 267 | .PARAMETER SleepTimer 268 | Milliseconds to sleep after checking for completed runspaces and in a few other spots. I would not recommend dropping below 200 or increasing above 500 269 | 270 | .PARAMETER RunspaceTimeout 271 | Maximum time in seconds a single thread can run. If execution of your code takes longer than this, it is disposed. Default: 0 (seconds) 272 | 273 | WARNING: Using this parameter requires that maxQueue be set to throttle (it will be by default) for accurate timing. Details here: 274 | http://gallery.technet.microsoft.com/Run-Parallel-Parallel-377fd430 275 | 276 | .PARAMETER NoCloseOnTimeout 277 | Do not dispose of timed out tasks or attempt to close the runspace if threads have timed out. This will prevent the script from hanging in certain situations where threads become non-responsive, at the expense of leaking memory within the PowerShell host. 278 | 279 | .PARAMETER MaxQueue 280 | Maximum number of powershell instances to add to runspace pool. If this is higher than $throttle, $timeout will be inaccurate 281 | 282 | If this is equal or less than throttle, there will be a performance impact 283 | 284 | The default value is $throttle times 3, if $runspaceTimeout is not specified 285 | The default value is $throttle, if $runspaceTimeout is specified 286 | 287 | .PARAMETER LogFile 288 | Path to a file where we can log results, including run time for each thread, whether it completes, completes with errors, or times out. 289 | 290 | .PARAMETER AppendLog 291 | Append to existing log 292 | 293 | .PARAMETER Quiet 294 | Disable progress bar 295 | 296 | .EXAMPLE 297 | Each example uses Test-ForPacs.ps1 which includes the following code: 298 | param($computer) 299 | 300 | if(test-connection $computer -count 1 -quiet -BufferSize 16){ 301 | $object = [pscustomobject] @{ 302 | Computer=$computer; 303 | Available=1; 304 | Kodak=$( 305 | if((test-path "\\$computer\c$\users\public\desktop\Kodak Direct View Pacs.url") -or (test-path "\\$computer\c$\documents and settings\all users\desktop\Kodak Direct View Pacs.url") ){"1"}else{"0"} 306 | ) 307 | } 308 | } 309 | else{ 310 | $object = [pscustomobject] @{ 311 | Computer=$computer; 312 | Available=0; 313 | Kodak="NA" 314 | } 315 | } 316 | 317 | $object 318 | 319 | .EXAMPLE 320 | Invoke-Parallel -scriptfile C:\public\Test-ForPacs.ps1 -inputobject $(get-content C:\pcs.txt) -runspaceTimeout 10 -throttle 10 321 | 322 | Pulls list of PCs from C:\pcs.txt, 323 | Runs Test-ForPacs against each 324 | If any query takes longer than 10 seconds, it is disposed 325 | Only run 10 threads at a time 326 | 327 | .EXAMPLE 328 | Invoke-Parallel -scriptfile C:\public\Test-ForPacs.ps1 -inputobject c-is-ts-91, c-is-ts-95 329 | 330 | Runs against c-is-ts-91, c-is-ts-95 (-computername) 331 | Runs Test-ForPacs against each 332 | 333 | .EXAMPLE 334 | $stuff = [pscustomobject] @{ 335 | ContentFile = "windows\system32\drivers\etc\hosts" 336 | Logfile = "C:\temp\log.txt" 337 | } 338 | 339 | $computers | Invoke-Parallel -parameter $stuff { 340 | $contentFile = join-path "\\$_\c$" $parameter.contentfile 341 | Get-Content $contentFile | 342 | set-content $parameter.logfile 343 | } 344 | 345 | This example uses the parameter argument. This parameter is a single object. To pass multiple items into the script block, we create a custom object (using a PowerShell v3 language) with properties we want to pass in. 346 | 347 | Inside the script block, $parameter is used to reference this parameter object. This example sets a content file, gets content from that file, and sets it to a predefined log file. 348 | 349 | .EXAMPLE 350 | $test = 5 351 | 1..2 | Invoke-Parallel -ImportVariables {$_ * $test} 352 | 353 | Add variables from the current session to the session state. Without -ImportVariables $Test would not be accessible 354 | 355 | .EXAMPLE 356 | $test = 5 357 | 1..2 | Invoke-Parallel {$_ * $Using:test} 358 | 359 | Reference a variable from the current session with the $Using: syntax. Requires PowerShell 3 or later. Note that -ImportVariables parameter is no longer necessary. 360 | 361 | .FUNCTIONALITY 362 | PowerShell Language 363 | 364 | .NOTES 365 | Credit to Boe Prox for the base runspace code and $Using implementation 366 | http://learn-powershell.net/2012/05/10/speedy-network-information-query-using-powershell/ 367 | http://gallery.technet.microsoft.com/scriptcenter/Speedy-Network-Information-5b1406fb#content 368 | https://github.com/proxb/PoshRSJob/ 369 | 370 | Credit to T Bryce Yehl for the Quiet and NoCloseOnTimeout implementations 371 | 372 | Credit to Sergei Vorobev for the many ideas and contributions that have improved functionality, reliability, and ease of use 373 | 374 | .LINK 375 | https://github.com/RamblingCookieMonster/Invoke-Parallel 376 | #> 377 | [cmdletbinding(DefaultParameterSetName = 'ScriptBlock')] 378 | Param ( 379 | [Parameter(Mandatory = $false, position = 0, ParameterSetName = 'ScriptBlock')] 380 | [System.Management.Automation.ScriptBlock]$ScriptBlock, 381 | 382 | [Parameter(Mandatory = $false, ParameterSetName = 'ScriptFile')] 383 | [ValidateScript( {Test-Path $_ -pathtype leaf})] 384 | $ScriptFile, 385 | 386 | [Parameter(Mandatory = $true, ValueFromPipeline = $true)] 387 | [Alias('CN', '__Server', 'IPAddress', 'Server', 'ComputerName')] 388 | [PSObject]$InputObject, 389 | 390 | [PSObject]$Parameter, 391 | 392 | [switch]$ImportVariables, 393 | [switch]$ImportModules, 394 | [switch]$ImportFunctions, 395 | 396 | [int]$Throttle = 20, 397 | [int]$SleepTimer = 200, 398 | [int]$RunspaceTimeout = 0, 399 | [switch]$NoCloseOnTimeout = $false, 400 | [int]$MaxQueue, 401 | 402 | [validatescript( {Test-Path (Split-Path $_ -parent)})] 403 | [switch] $AppendLog = $false, 404 | [string]$LogFile, 405 | 406 | [switch] $Quiet = $false 407 | ) 408 | begin { 409 | #No max queue specified? Estimate one. 410 | #We use the script scope to resolve an odd PowerShell 2 issue where MaxQueue isn't seen later in the function 411 | if ( -not $PSBoundParameters.ContainsKey('MaxQueue') ) { 412 | if ($RunspaceTimeout -ne 0) { $script:MaxQueue = $Throttle } 413 | else { $script:MaxQueue = $Throttle * 3 } 414 | } 415 | else { 416 | $script:MaxQueue = $MaxQueue 417 | } 418 | $ProgressId = Get-Random 419 | Write-Verbose "Throttle: '$throttle' SleepTimer '$sleepTimer' runSpaceTimeout '$runspaceTimeout' maxQueue '$maxQueue' logFile '$logFile'" 420 | 421 | #If they want to import variables or modules, create a clean runspace, get loaded items, use those to exclude items 422 | if ($ImportVariables -or $ImportModules -or $ImportFunctions) { 423 | $StandardUserEnv = [powershell]::Create().addscript( { 424 | 425 | #Get modules, snapins, functions in this clean runspace 426 | $Modules = Get-Module | Select-Object -ExpandProperty Name 427 | $Snapins = Get-PSSnapin | Select-Object -ExpandProperty Name 428 | $Functions = Get-ChildItem function:\ | Select-Object -ExpandProperty Name 429 | 430 | #Get variables in this clean runspace 431 | #Called last to get vars like $? into session 432 | $Variables = Get-Variable | Select-Object -ExpandProperty Name 433 | 434 | #Return a hashtable where we can access each. 435 | @{ 436 | Variables = $Variables 437 | Modules = $Modules 438 | Snapins = $Snapins 439 | Functions = $Functions 440 | } 441 | }).invoke()[0] 442 | 443 | if ($ImportVariables) { 444 | #Exclude common parameters, bound parameters, and automatic variables 445 | Function _temp {[cmdletbinding(SupportsShouldProcess = $True)] param() } 446 | $VariablesToExclude = @( (Get-Command _temp | Select-Object -ExpandProperty parameters).Keys + $PSBoundParameters.Keys + $StandardUserEnv.Variables ) 447 | Write-Verbose "Excluding variables $( ($VariablesToExclude | Sort-Object ) -join ", ")" 448 | 449 | # we don't use 'Get-Variable -Exclude', because it uses regexps. 450 | # One of the veriables that we pass is '$?'. 451 | # There could be other variables with such problems. 452 | # Scope 2 required if we move to a real module 453 | $UserVariables = @( Get-Variable | Where-Object { -not ($VariablesToExclude -contains $_.Name) } ) 454 | Write-Verbose "Found variables to import: $( ($UserVariables | Select-Object -expandproperty Name | Sort-Object ) -join ", " | Out-String).`n" 455 | } 456 | if ($ImportModules) { 457 | $UserModules = @( Get-Module | Where-Object {$StandardUserEnv.Modules -notcontains $_.Name -and (Test-Path $_.Path -ErrorAction SilentlyContinue)} | Select-Object -ExpandProperty Path ) 458 | $UserSnapins = @( Get-PSSnapin | Select-Object -ExpandProperty Name | Where-Object {$StandardUserEnv.Snapins -notcontains $_ } ) 459 | } 460 | if ($ImportFunctions) { 461 | $UserFunctions = @( Get-ChildItem function:\ | Where-Object { $StandardUserEnv.Functions -notcontains $_.Name } ) 462 | } 463 | } 464 | 465 | #region functions 466 | Function Get-RunspaceData { 467 | [cmdletbinding()] 468 | param( [switch]$Wait ) 469 | #loop through runspaces 470 | #if $wait is specified, keep looping until all complete 471 | Do { 472 | #set more to false for tracking completion 473 | $more = $false 474 | 475 | #Progress bar if we have inputobject count (bound parameter) 476 | if (-not $Quiet) { 477 | Write-Progress -Id $ProgressId -Activity "Running Query" -Status "Starting threads"` 478 | -CurrentOperation "$startedCount threads defined - $totalCount input objects - $script:completedCount input objects processed"` 479 | -PercentComplete $( Try { $script:completedCount / $totalCount * 100 } Catch {0} ) 480 | } 481 | 482 | #run through each runspace. 483 | Foreach ($runspace in $runspaces) { 484 | 485 | #get the duration - inaccurate 486 | $currentdate = Get-Date 487 | $runtime = $currentdate - $runspace.startTime 488 | $runMin = [math]::Round( $runtime.totalminutes , 2 ) 489 | 490 | #set up log object 491 | $log = "" | Select-Object Date, Action, Runtime, Status, Details 492 | $log.Action = "Removing:'$($runspace.object)'" 493 | $log.Date = $currentdate 494 | $log.Runtime = "$runMin minutes" 495 | 496 | #If runspace completed, end invoke, dispose, recycle, counter++ 497 | If ($runspace.Runspace.isCompleted) { 498 | 499 | $script:completedCount++ 500 | 501 | #check if there were errors 502 | if ($runspace.powershell.Streams.Error.Count -gt 0) { 503 | #set the logging info and move the file to completed 504 | $log.status = "CompletedWithErrors" 505 | Write-Verbose ($log | ConvertTo-Csv -Delimiter ";" -NoTypeInformation)[1] 506 | foreach ($ErrorRecord in $runspace.powershell.Streams.Error) { 507 | Write-Error -ErrorRecord $ErrorRecord 508 | } 509 | } 510 | else { 511 | #add logging details and cleanup 512 | $log.status = "Completed" 513 | Write-Verbose ($log | ConvertTo-Csv -Delimiter ";" -NoTypeInformation)[1] 514 | } 515 | 516 | #everything is logged, clean up the runspace 517 | $runspace.powershell.EndInvoke($runspace.Runspace) 518 | $runspace.powershell.dispose() 519 | $runspace.Runspace = $null 520 | $runspace.powershell = $null 521 | } 522 | #If runtime exceeds max, dispose the runspace 523 | ElseIf ( $runspaceTimeout -ne 0 -and $runtime.totalseconds -gt $runspaceTimeout) { 524 | $script:completedCount++ 525 | $timedOutTasks = $true 526 | 527 | #add logging details and cleanup 528 | $log.status = "TimedOut" 529 | Write-Verbose ($log | ConvertTo-Csv -Delimiter ";" -NoTypeInformation)[1] 530 | Write-Error "Runspace timed out at $($runtime.totalseconds) seconds for the object:`n$($runspace.object | out-string)" 531 | 532 | #Depending on how it hangs, we could still get stuck here as dispose calls a synchronous method on the powershell instance 533 | if (!$noCloseOnTimeout) { $runspace.powershell.dispose() } 534 | $runspace.Runspace = $null 535 | $runspace.powershell = $null 536 | $completedCount++ 537 | } 538 | 539 | #If runspace isn't null set more to true 540 | ElseIf ($runspace.Runspace -ne $null ) { 541 | $log = $null 542 | $more = $true 543 | } 544 | 545 | #log the results if a log file was indicated 546 | if ($logFile -and $log) { 547 | ($log | ConvertTo-Csv -Delimiter ";" -NoTypeInformation)[1] | out-file $LogFile -append 548 | } 549 | } 550 | 551 | #Clean out unused runspace jobs 552 | $temphash = $runspaces.clone() 553 | $temphash | Where-Object { $_.runspace -eq $Null } | ForEach-Object { 554 | $Runspaces.remove($_) 555 | } 556 | 557 | #sleep for a bit if we will loop again 558 | if ($PSBoundParameters['Wait']) { Start-Sleep -milliseconds $SleepTimer } 559 | 560 | #Loop again only if -wait parameter and there are more runspaces to process 561 | } while ($more -and $PSBoundParameters['Wait']) 562 | 563 | #End of runspace function 564 | } 565 | #endregion functions 566 | 567 | #region Init 568 | 569 | if ($PSCmdlet.ParameterSetName -eq 'ScriptFile') { 570 | $ScriptBlock = [scriptblock]::Create( $(Get-Content $ScriptFile | out-string) ) 571 | } 572 | elseif ($PSCmdlet.ParameterSetName -eq 'ScriptBlock') { 573 | #Start building parameter names for the param block 574 | [string[]]$ParamsToAdd = '$_' 575 | if ( $PSBoundParameters.ContainsKey('Parameter') ) { 576 | $ParamsToAdd += '$Parameter' 577 | } 578 | 579 | $UsingVariableData = $Null 580 | 581 | # This code enables $Using support through the AST. 582 | # This is entirely from Boe Prox, and his https://github.com/proxb/PoshRSJob module; all credit to Boe! 583 | 584 | if ($PSVersionTable.PSVersion.Major -gt 2) { 585 | #Extract using references 586 | $UsingVariables = $ScriptBlock.ast.FindAll( {$args[0] -is [System.Management.Automation.Language.UsingExpressionAst]}, $True) 587 | 588 | If ($UsingVariables) { 589 | $List = New-Object 'System.Collections.Generic.List`1[System.Management.Automation.Language.VariableExpressionAst]' 590 | ForEach ($Ast in $UsingVariables) { 591 | [void]$list.Add($Ast.SubExpression) 592 | } 593 | 594 | $UsingVar = $UsingVariables | Group-Object -Property SubExpression | ForEach-Object {$_.Group | Select-Object -First 1} 595 | 596 | #Extract the name, value, and create replacements for each 597 | $UsingVariableData = ForEach ($Var in $UsingVar) { 598 | try { 599 | $Value = Get-Variable -Name $Var.SubExpression.VariablePath.UserPath -ErrorAction Stop 600 | [pscustomobject]@{ 601 | Name = $Var.SubExpression.Extent.Text 602 | Value = $Value.Value 603 | NewName = ('$__using_{0}' -f $Var.SubExpression.VariablePath.UserPath) 604 | NewVarName = ('__using_{0}' -f $Var.SubExpression.VariablePath.UserPath) 605 | } 606 | } 607 | catch { 608 | Write-Error "$($Var.SubExpression.Extent.Text) is not a valid Using: variable!" 609 | } 610 | } 611 | $ParamsToAdd += $UsingVariableData | Select-Object -ExpandProperty NewName -Unique 612 | 613 | $NewParams = $UsingVariableData.NewName -join ', ' 614 | $Tuple = [Tuple]::Create($list, $NewParams) 615 | $bindingFlags = [Reflection.BindingFlags]"Default,NonPublic,Instance" 616 | $GetWithInputHandlingForInvokeCommandImpl = ($ScriptBlock.ast.gettype().GetMethod('GetWithInputHandlingForInvokeCommandImpl', $bindingFlags)) 617 | 618 | $StringScriptBlock = $GetWithInputHandlingForInvokeCommandImpl.Invoke($ScriptBlock.ast, @($Tuple)) 619 | 620 | $ScriptBlock = [scriptblock]::Create($StringScriptBlock) 621 | 622 | Write-Verbose $StringScriptBlock 623 | } 624 | } 625 | 626 | $ScriptBlock = $ExecutionContext.InvokeCommand.NewScriptBlock("param($($ParamsToAdd -Join ", "))`r`n" + $Scriptblock.ToString()) 627 | } 628 | else { 629 | Throw "Must provide ScriptBlock or ScriptFile"; Break 630 | } 631 | 632 | Write-Debug "`$ScriptBlock: $($ScriptBlock | Out-String)" 633 | Write-Verbose "Creating runspace pool and session states" 634 | 635 | #If specified, add variables and modules/snapins to session state 636 | $sessionstate = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault() 637 | if ($ImportVariables -and $UserVariables.count -gt 0) { 638 | foreach ($Variable in $UserVariables) { 639 | $sessionstate.Variables.Add((New-Object -TypeName System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList $Variable.Name, $Variable.Value, $null) ) 640 | } 641 | } 642 | if ($ImportModules) { 643 | if ($UserModules.count -gt 0) { 644 | foreach ($ModulePath in $UserModules) { 645 | $sessionstate.ImportPSModule($ModulePath) 646 | } 647 | } 648 | if ($UserSnapins.count -gt 0) { 649 | foreach ($PSSnapin in $UserSnapins) { 650 | [void]$sessionstate.ImportPSSnapIn($PSSnapin, [ref]$null) 651 | } 652 | } 653 | } 654 | if ($ImportFunctions -and $UserFunctions.count -gt 0) { 655 | foreach ($FunctionDef in $UserFunctions) { 656 | $sessionstate.Commands.Add((New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry -ArgumentList $FunctionDef.Name, $FunctionDef.ScriptBlock)) 657 | } 658 | } 659 | 660 | #Create runspace pool 661 | $runspacepool = [runspacefactory]::CreateRunspacePool(1, $Throttle, $sessionstate, $Host) 662 | $runspacepool.Open() 663 | 664 | Write-Verbose "Creating empty collection to hold runspace jobs" 665 | $Script:runspaces = New-Object System.Collections.ArrayList 666 | 667 | #If inputObject is bound get a total count and set bound to true 668 | $bound = $PSBoundParameters.keys -contains "InputObject" 669 | if (-not $bound) { 670 | [System.Collections.ArrayList]$allObjects = @() 671 | } 672 | 673 | #Set up log file if specified 674 | if ( $LogFile -and (-not (Test-Path $LogFile) -or $AppendLog -eq $false)) { 675 | New-Item -ItemType file -Path $logFile -Force | Out-Null 676 | ("" | Select-Object -Property Date, Action, Runtime, Status, Details | ConvertTo-Csv -NoTypeInformation -Delimiter ";")[0] | Out-File $LogFile 677 | } 678 | 679 | #write initial log entry 680 | $log = "" | Select-Object -Property Date, Action, Runtime, Status, Details 681 | $log.Date = Get-Date 682 | $log.Action = "Batch processing started" 683 | $log.Runtime = $null 684 | $log.Status = "Started" 685 | $log.Details = $null 686 | if ($logFile) { 687 | ($log | convertto-csv -Delimiter ";" -NoTypeInformation)[1] | Out-File $LogFile -Append 688 | } 689 | $timedOutTasks = $false 690 | #endregion INIT 691 | } 692 | process { 693 | #add piped objects to all objects or set all objects to bound input object parameter 694 | if ($bound) { 695 | $allObjects = $InputObject 696 | } 697 | else { 698 | [void]$allObjects.add( $InputObject ) 699 | } 700 | } 701 | end { 702 | #Use Try/Finally to catch Ctrl+C and clean up. 703 | try { 704 | #counts for progress 705 | $totalCount = $allObjects.count 706 | $script:completedCount = 0 707 | $startedCount = 0 708 | foreach ($object in $allObjects) { 709 | #region add scripts to runspace pool 710 | #Create the powershell instance, set verbose if needed, supply the scriptblock and parameters 711 | $powershell = [powershell]::Create() 712 | 713 | if ($VerbosePreference -eq 'Continue') { 714 | [void]$PowerShell.AddScript( {$VerbosePreference = 'Continue'}) 715 | } 716 | 717 | [void]$PowerShell.AddScript($ScriptBlock).AddArgument($object) 718 | 719 | if ($parameter) { 720 | [void]$PowerShell.AddArgument($parameter) 721 | } 722 | 723 | # $Using support from Boe Prox 724 | if ($UsingVariableData) { 725 | Foreach ($UsingVariable in $UsingVariableData) { 726 | Write-Verbose "Adding $($UsingVariable.Name) with value: $($UsingVariable.Value)" 727 | [void]$PowerShell.AddArgument($UsingVariable.Value) 728 | } 729 | } 730 | 731 | #Add the runspace into the powershell instance 732 | $powershell.RunspacePool = $runspacepool 733 | 734 | #Create a temporary collection for each runspace 735 | $temp = "" | Select-Object PowerShell, StartTime, object, Runspace 736 | $temp.PowerShell = $powershell 737 | $temp.StartTime = Get-Date 738 | $temp.object = $object 739 | 740 | #Save the handle output when calling BeginInvoke() that will be used later to end the runspace 741 | $temp.Runspace = $powershell.BeginInvoke() 742 | $startedCount++ 743 | 744 | #Add the temp tracking info to $runspaces collection 745 | Write-Verbose ( "Adding {0} to collection at {1}" -f $temp.object, $temp.starttime.tostring() ) 746 | $runspaces.Add($temp) | Out-Null 747 | 748 | #loop through existing runspaces one time 749 | Get-RunspaceData 750 | 751 | #If we have more running than max queue (used to control timeout accuracy) 752 | #Script scope resolves odd PowerShell 2 issue 753 | $firstRun = $true 754 | while ($runspaces.count -ge $Script:MaxQueue) { 755 | #give verbose output 756 | if ($firstRun) { 757 | Write-Verbose "$($runspaces.count) items running - exceeded $Script:MaxQueue limit." 758 | } 759 | $firstRun = $false 760 | 761 | #run get-runspace data and sleep for a short while 762 | Get-RunspaceData 763 | Start-Sleep -Milliseconds $sleepTimer 764 | } 765 | #endregion add scripts to runspace pool 766 | } 767 | Write-Verbose ( "Finish processing the remaining runspace jobs: {0}" -f ( @($runspaces | Where-Object {$_.Runspace -ne $Null}).Count) ) 768 | 769 | Get-RunspaceData -wait 770 | if (-not $quiet) { 771 | Write-Progress -Id $ProgressId -Activity "Running Query" -Status "Starting threads" -Completed 772 | } 773 | } 774 | finally { 775 | #Close the runspace pool, unless we specified no close on timeout and something timed out 776 | if ( ($timedOutTasks -eq $false) -or ( ($timedOutTasks -eq $true) -and ($noCloseOnTimeout -eq $false) ) ) { 777 | Write-Verbose "Closing the runspace pool" 778 | $runspacepool.close() 779 | } 780 | #collect garbage 781 | [gc]::Collect() 782 | } 783 | } 784 | } 785 | #Mount-FslDisk 786 | function Mount-FslDisk { 787 | [CmdletBinding()] 788 | 789 | Param ( 790 | [Parameter( 791 | Position = 1, 792 | ValuefromPipelineByPropertyName = $true, 793 | ValuefromPipeline = $true, 794 | Mandatory = $true 795 | )] 796 | [alias('FullName')] 797 | [System.String]$Path, 798 | 799 | [Parameter( 800 | ValuefromPipelineByPropertyName = $true 801 | )] 802 | [Switch]$PassThru 803 | ) 804 | 805 | BEGIN { 806 | Set-StrictMode -Version Latest 807 | } # Begin 808 | PROCESS { 809 | 810 | # FSLogix Disk Partition Number this won't work with vhds created with MS tools as their main partition number is 2 811 | $partitionNumber = 1 812 | 813 | try { 814 | # Mount the disk without a drive letter and get it's info, Mount-DiskImage is used to remove reliance on Hyper-V tools 815 | $mountedDisk = Mount-DiskImage -ImagePath $Path -NoDriveLetter -PassThru -ErrorAction Stop | Get-DiskImage -ErrorAction Stop 816 | } 817 | catch { 818 | Write-Error "Failed to mount disk $Path" 819 | return 820 | } 821 | 822 | # Assign vhd to a random path in temp folder so we don't have to worry about free drive letters which can be horrible 823 | # New-Guid not used here for PoSh 3 compatibility 824 | $tempGUID = [guid]::NewGuid().ToString() 825 | $mountPath = Join-Path $Env:Temp ('FSLogixMnt-' + $tempGUID) 826 | 827 | try { 828 | # Create directory which we will mount too 829 | New-Item -Path $mountPath -ItemType Directory -ErrorAction Stop | Out-Null 830 | } 831 | catch { 832 | Write-Error "Failed to create mounting directory $mountPath" 833 | # Cleanup 834 | $mountedDisk | Dismount-DiskImage -ErrorAction SilentlyContinue 835 | return 836 | } 837 | 838 | try { 839 | 840 | $addPartitionAccessPathParams = @{ 841 | DiskNumber = $mountedDisk.Number 842 | PartitionNumber = $partitionNumber 843 | AccessPath = $mountPath 844 | ErrorAction = 'Stop' 845 | } 846 | 847 | Add-PartitionAccessPath @addPartitionAccessPathParams 848 | } 849 | catch { 850 | Write-Error "Failed to create junction point to $mountPath" 851 | # Cleanup 852 | Remove-Item -Path $mountPath -ErrorAction SilentlyContinue 853 | $mountedDisk | Dismount-DiskImage -ErrorAction SilentlyContinue 854 | return 855 | } 856 | 857 | if ($PassThru) { 858 | # Create output required for piping to Dismount-FslDisk 859 | $output = [PSCustomObject]@{ 860 | Path = $mountPath 861 | DiskNumber = $mountedDisk.Number 862 | ImagePath = $mountedDisk.ImagePath 863 | } 864 | Write-Output $output 865 | } 866 | Write-Verbose "Mounted $Path" 867 | } #Process 868 | END { 869 | 870 | } #End 871 | } #function Mount-FslDisk 872 | #Dismount-FslDisk 873 | function Dismount-FslDisk { 874 | [CmdletBinding()] 875 | 876 | Param ( 877 | [Parameter( 878 | Position = 1, 879 | ValuefromPipelineByPropertyName = $true, 880 | ValuefromPipeline = $true, 881 | Mandatory = $true 882 | )] 883 | [String]$Path, 884 | 885 | [Parameter( 886 | ValuefromPipelineByPropertyName = $true, 887 | ValuefromPipeline = $true, 888 | Mandatory = $true 889 | )] 890 | [int16]$DiskNumber, 891 | 892 | [Parameter( 893 | ValuefromPipelineByPropertyName = $true, 894 | Mandatory = $true 895 | )] 896 | [String]$ImagePath, 897 | 898 | [Parameter( 899 | ValuefromPipelineByPropertyName = $true 900 | )] 901 | [Switch]$PassThru 902 | ) 903 | 904 | BEGIN { 905 | Set-StrictMode -Version Latest 906 | } # Begin 907 | PROCESS { 908 | 909 | # FSLogix Disk Partition Number this won't work with vhds created with MS tools as their main partition number is 2 910 | $partitionNumber = 1 911 | 912 | if ($PassThru) { 913 | $junctionPointRemoved = $false 914 | $mountRemoved = $false 915 | $directoryRemoved = $false 916 | } 917 | 918 | # Reverse the three tasks from Mount-FslDisk 919 | try { 920 | Remove-PartitionAccessPath -DiskNumber $DiskNumber -PartitionNumber $partitionNumber -AccessPath $Path -ErrorAction Stop | Out-Null 921 | $junctionPointRemoved = $true 922 | } 923 | catch { 924 | Write-Error "Failed to remove the junction point to $Path" 925 | } 926 | 927 | try { 928 | Dismount-DiskImage -ImagePath $ImagePath -ErrorAction Stop | Out-Null 929 | $mountRemoved = $true 930 | } 931 | catch { 932 | Write-Error "Failed to dismount disk $ImagePath" 933 | } 934 | 935 | try { 936 | Remove-Item -Path $Path -ErrorAction Stop | Out-Null 937 | $directoryRemoved = $true 938 | } 939 | catch { 940 | Write-Error "Failed to delete temp mount directory $Path" 941 | } 942 | 943 | If ($PassThru) { 944 | $output = [PSCustomObject]@{ 945 | JunctionPointRemoved = $junctionPointRemoved 946 | MountRemoved = $mountRemoved 947 | DirectoryRemoved = $directoryRemoved 948 | } 949 | Write-Output $output 950 | } 951 | Write-Verbose "Dismounted $ImagePath" 952 | } #Process 953 | END {} #End 954 | } #function Dismount-FslDisk 955 | function Remove-FslMultiOst { 956 | [CmdletBinding()] 957 | 958 | Param ( 959 | [Parameter( 960 | Position = 0, 961 | ValuefromPipelineByPropertyName = $true, 962 | ValuefromPipeline = $true, 963 | Mandatory = $true 964 | )] 965 | [System.String]$Path 966 | ) 967 | 968 | BEGIN { 969 | Set-StrictMode -Version Latest 970 | } # Begin 971 | PROCESS { 972 | #Write-Log "Getting ost files from $Path" 973 | $ost = Get-ChildItem -Path (Join-Path $Path *.ost) 974 | if ($null -eq $ost) { 975 | #Write-log -level Warn "Did not find any ost files in $Path" 976 | #$ostDelNum = 0 977 | } 978 | else { 979 | 980 | $count = $ost | Measure-Object 981 | 982 | if ($count.Count -gt 1) { 983 | 984 | $mailboxes = $ost.BaseName.trimend('(', ')', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0') | Group-Object | Select-Object -ExpandProperty Name 985 | 986 | foreach ($mailbox in $mailboxes) { 987 | $mailboxOst = $ost | Where-Object {$_.BaseName.StartsWith($mailbox)} 988 | 989 | $count = $mailboxOst | Measure-Object 990 | 991 | #Write-Log "Found $count ost files for $mailbox" 992 | 993 | if ($count -gt 1) { 994 | 995 | $ostDelNum = $count - 1 996 | #Write-Log "Deleting $ostDelNum ost files" 997 | try { 998 | $latestOst = $mailboxOst | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1 999 | $mailboxOst | Where-Object {$_.Name -ne $latestOst.Name} | Remove-Item -Force -ErrorAction Stop 1000 | } 1001 | catch { 1002 | #Write-log -level Error "Failed to delete ost files in $vhd for $mailbox" 1003 | } 1004 | } 1005 | else { 1006 | #Write-Log "Only One ost file found for $mailbox. No action taken" 1007 | $ostDelNum = 0 1008 | } 1009 | 1010 | } 1011 | } 1012 | } 1013 | } #Process 1014 | END {} #End 1015 | } #function Remove-FslMultiOst 1016 | 1017 | $PSDefaultParameterValues = @{ "Write-Log:Path" = $LogFilePath } 1018 | 1019 | $usableThreads = (Get-Ciminstance Win32_processor).ThreadCount - 2 1020 | If ($usableThreads -le 2) { $usableThreads = 2 } 1021 | } # Begin 1022 | PROCESS { 1023 | 1024 | if (-not (Test-Path $Path)) { 1025 | Write-Error "$Path not found" 1026 | break 1027 | } 1028 | 1029 | if ($Recurse) { 1030 | $listing = Get-ChildItem $Path -Recurse 1031 | } 1032 | else { 1033 | $listing = Get-ChildItem $Path 1034 | } 1035 | 1036 | $diskList = $listing | Where-Object { $_.extension -in ".vhd", ".vhdx" } 1037 | 1038 | 1039 | if ( ($diskList | Measure-Object).count -eq 0 ) { 1040 | Write-Warning "No files to process" 1041 | } 1042 | 1043 | 1044 | $scriptblock = { 1045 | 1046 | Param ( $disk ) 1047 | 1048 | $PSDefaultParameterValues = @{ "Write-Log:Path" = $LogFilePath } 1049 | 1050 | switch ($true) { 1051 | $DeleteOlderThanDays { 1052 | if ($disk.LastAccessTime -lt (Get-Date).AddDays(-$DeleteOlderThanDays) ) { 1053 | try { 1054 | Remove-Item -ErrorAction Stop 1055 | } 1056 | catch { 1057 | Write-Log -Level Error "Could Not Delete $disk" 1058 | } 1059 | } 1060 | break 1061 | } 1062 | $IgnoreLessThanGB { 1063 | if ($disk.size -lt $IgnoreLessThanGB) { 1064 | Write-Log "$disk smaller than $IgnoreLessThanGB no action taken" 1065 | break 1066 | } 1067 | } 1068 | Default { 1069 | try { 1070 | $mount = Mount-FslDisk -Path $disk -PassThru 1071 | 1072 | Remove-FslMultiOst -Path $mount.Path 1073 | 1074 | $partitionsize = Get-PartitionSupportedSize -DiskNumber $mount.DiskNumber 1075 | 1076 | if ($partitionsize.SizeMin / $partitionsize.SizeMax -lt 0.8 ) { 1077 | Resize-Partition -DiskNumber $mount.DiskNumber -Size $n.SizeMin 1078 | $mount | DisMount-FslDisk 1079 | Resize-VHD $disk -ToMinimumSize 1080 | Optimize-VHD $disk 1081 | #Resize-VHD $Disk -SizeBytes 62914560000 1082 | $mount = Mount-FslDisk -Path $disk -PassThru 1083 | $partitionInfo = Get-PartitionSupportedSize -DiskNumber $mount.DiskNumber 1084 | Resize-Partition -DiskNumber $mount.DiskNumber -Size $partitionInfo.SizeMax 1085 | $mount | DisMount-FslDisk 1086 | } 1087 | else { 1088 | $mount | DisMount-FslDisk 1089 | Write-Log "$disk not resized due to insufficient free space" 1090 | } 1091 | } 1092 | catch { 1093 | $error[0] | Write-Log 1094 | Write-Log -Level Error "Could not resize $disk" 1095 | } 1096 | } 1097 | } 1098 | } #Scriptblock 1099 | 1100 | $diskList | Invoke-Parallel -ScriptBlock $scriptblock -Throttle $usableThreads -ImportFunctions -ImportVariables -ImportModules 1101 | 1102 | } #Process 1103 | END {} #End 1104 | } #function Invoke-FslShrinkDisk --------------------------------------------------------------------------------