Public/Optimize-VirtualDisk.ps1

<#
.SYNOPSIS
    Performs defragmenting and optimization operations on a virtual hard disk file.
 
.DESCRIPTION
    Defragments partitions of a virtual hard disk using the Optimize-Volume cmdlet and optimizes the virtual hard disk file using Optimize-VHD.
 
    This cmdlet can be used on virtual hard disk files (VHD or VHDX) on disk, or on virtual machines.
 
    When targeting virtual machines, the virtual machine will have its virtual hard disk files defragmented and optimized only if the virtual machine does not have a snapshot, and if its virtual hard disk files are not part of another virtual machine's snapshot.
 
    ------------- The Optimization Process -------------
 
    Only partitions with drive letters (e.g.: C:\) will be defragmented. Partitions without drive letters will be skipped.
 
    For each valid partition on the virtual hard disk the following commands will be ran:
 
        Optimize-Volume -Defrag is ran on the partition.
            All files on the partition are defragmented to be contiguous
 
        Optimize-Volume -SlabConsolidate -ReTrim is ran on the partition.
            All defragmented data is consolidated towards the start of the partition and excess file size is trimmed.
 
        Optimize-Volume -Defrag is ran on the partition.
            The partition again is defragmented again to ensure no leftover fragmentation is present from moving data during the slab consolidation and re-trim.
 
        Optimize-Volume -SlabConsolidate is ran on the partition.
            All defragmented data is consolidated once more towards the start of the partition.
 
    After all partitions are defragmented the optimization concludes with running Optimize-VHD on the virtual hard disk.
 
.NOTES
    DO NOT run this cmdlet in a for loop.
 
        If you are planning to work on several VHD files or VMs at once then use piping (see INPUTS section or EXAMPLES) to iterate over all of them, otherwise each call to the cmdlet will incur a several second delay depending on the amount of VMs present on the system while the list of VHD files on disk used by VM snapshots is generated.
 
    This cmdlet must be run as an administrator.
 
    Both VHD and VHDX files are supported. AVHDX files ARE NOT supported.
 
    The system must have enough drive letters available to mount all partitions with drive letters on the virtual hard disk (e.g.: a virtual hard disk with C:\, D:\ and E:\ would required the system to have 3 drive letters available).
 
.PARAMETER Path
    The path to the virtual hard disk file(s) to optimize.
 
    This path may include wildcard characters such as * to target multiple files (e.g.: "*.vhdx").
 
    This path may also point to a directory, in which case the files in the directory are targeted based on the -Filter parameter, which defaults to "*.vhdx".
 
    When piping input, any strings piped into the cmdlet will be treated as -Path entries.
 
.PARAMETER VMName
    The name of the virtual machine to optimize.
 
    See the VM parameter for more information.
 
.PARAMETER VM
    The virtual machine to optimize.
 
    The object passed to this parameter must be of the type [Microsoft.HyperV.PowerShell.VirtualMachine], which can be obtained using the Get-VM cmdlet in the Hyper-V module.
 
    The virtual machine MUST NOT have any snapshots and must not have any AVHDX virtual hard disk files, otherwise the cmdlet will raise an error.
 
    The virtual machine should be turned off prior to optimization. Alternatively, the -Shutdown switch can be used to tell Optimize-VirtualDisk to attempt to shut the virtual machine down prior to optimization, and then restart it after optimization finishes.
 
.PARAMETER Filter
    The filter to use when searching for virtual hard disk files to defragment.
 
    This only applies when searching directories in -Path and when piping directories to the cmdlet.
 
    This parameter is the same as the -Filter parameter in Get-ChildItem, and it supports wildcards (e.g.: "*.vhdx", "*.vhd?").
 
.PARAMETER Shutdown
    When working with virtual machines, if the virtual machine is on prior to optimization, the Optimize-VirtualDisk cmdlet will attempt to shut the virtual machine down and proceed with optimization.
 
    If the virtual machine is successfully shut down then the virtual machine will be restarted after optimization.
 
.PARAMETER SkipVMCheck
    Skips the cataloging of all virtual hard disk files used in snapshots.
 
    When the Optimize-VirtualDisk cmdlet is ran the cmdlet will iterate over all virtual machines returned from the Get-VM cmdlet and check if they have snapshots.
 
    In the event a virtual machine has a snapshot its virtual hard disk files' paths are recorded and checked later to ensure we don't accidentally try and optimize the volume (and thereby corrupt the AVHDX files using it).
 
.PARAMETER NoLogo
    Removes the larger AJ Tek logo from the script's output.
 
.PARAMETER InputObject
    Any object that is piped to the Optimize-VirtualDisk cmdlet.
 
    The object is handled differently depending on what type it is. See the INPUTS section for more details.
 
    This parameter IS NOT meant to be used, and you should instead use the appropriate parameter for your use case (e.g.: -Path) instead of using this parameter.
 
.EXAMPLE
    Optimize-VirtualDisk "C:\Hyper-V\Virtual Machines\Server 2022\Virtual Hard Disks\Server 2022.vhdx"
    Description: Optimizes the provided VHDX file "Server 2022.vhdx".
 
    Before executing, Optimize-VirtualDisk will gather a list of all VHD files used by snapshots of VMs on the system.
 
    If "Server 2022.vhdx" is used in a snapshot for a VM, Optimize-VirtualDisk will error out and will not try to optimize the VHD file as to prevent corruption.
 
.EXAMPLE
    Optimize-VirtualDisk "C:\Hyper-V\Virtual Machines\Server 2022\Virtual Hard Disks\Server 2022.vhdx","C:\Hyper-V\Virtual Machines\Server 2022\Virtual Hard Disks\Storage.vhdx"
    Description: Optimizes the provided VHDX files "Server 2022.vhdx" and "Storage.vhdx".
 
    Before executing, Optimize-VirtualDisk will gather a list of all VHD files used by snapshots of VMs on the system.
 
    If either VHD file is used in a snapshot for a VM, Optimize-VirtualDisk will error out and will not try to optimize the VHD file as to prevent corruption.
 
.EXAMPLE
    Optimize-VirtualDisk "C:\Hyper-V\Virtual Machines\Server 2022\Virtual Hard Disks\*.vhdx"
    Description: Optimizes the all VHDX files under "C:\Hyper-V\Virtual Machines\Server 2022\Virtual Hard Disks".
 
    Before executing, Optimize-VirtualDisk will gather a list of all VHD files used by snapshots of VMs on the system.
 
    If any VHD file targeted is used in a snapshot for a VM, Optimize-VirtualDisk will error out and will not try to optimize the VHD file as to prevent corruption.
 
.EXAMPLE
    Optimize-VirtualDisk "C:\Hyper-V\Virtual Machines\Server 2022\Virtual Hard Disks\*.vhdx" -WhatIf
    Description: Shows all VHDX files that would be optimized if the command was run without -WhatIf.
 
.EXAMPLE
    Optimize-VirtualDisk "C:\Hyper-V\Virtual Machines\Server 2022\Virtual Hard Disks\" -Filter "*.vhdx"
    Description: Optimizes the all VHDX files under "C:\Hyper-V\Virtual Machines\Server 2022\Virtual Hard Disks".
 
    This command functions similarly to if "Virtual Hard Disks\*.vhdx" was used instead - this is just an alternate way of doing the same thing.
 
    Before executing, Optimize-VirtualDisk will gather a list of all VHD files used by snapshots of VMs on the system.
 
    If any VHD file targeted is used in a snapshot for a VM, Optimize-VirtualDisk will error out and will not try to optimize the VHD file as to prevent corruption.
 
.EXAMPLE
    Optimize-VirtualDisk -VMName "Server 2022"
    Description: Optimizes all VHD files used by the "Server 2022" VM.
 
    The VM cannot have any snapshots.
 
    The VM must not be running and must not be in a saved state.
 
.EXAMPLE
    Optimize-VirtualDisk -VMName "Server 2022" -Shutdown
    Description: Optimizes all VHD files used by the "Server 2022" VM.
 
    The VM cannot have any snapshots.
 
    If the VM is running it will be shut down before optimization and then restarted afterwards.
 
.EXAMPLE
    Optimize-VirtualDisk -VM $server2022VM
    Description: Optimizes the VHD files used by the VM stored in the $server2022VM variable.
 
    The $server2022VM would contain a VirtualMachine object returned by Get-VM.
 
    It is generally recommended that you do not explicitly use the -VM parameter, and instead pipe the VM to Optimize-VirtualDisk. This is especially true if you are wanting to optimize multiple VMs, as the Optimize-VirtualDisk cmdlet SHOULD NOT be used in a for loop.
 
    The VM cannot have any snapshots.
 
    The VM must not be running and must not be in a saved state.
 
.EXAMPLE
    "C:\Hyper-V\Virtual Machines\Server 2022\Virtual Hard Disks\Server 2022.vhdx" | Optimize-VirtualDisk
    Description: Optimizes the VHD file "Server 2022.vhdx".
 
    Before executing, Optimize-VirtualDisk will gather a list of all VHD files used by snapshots of VMs on the system.
 
    If "Server 2022.vhdx" is used in a snapshot for a VM, Optimize-VirtualDisk will error out and will not try to optimize the VHD file as to prevent corruption.
 
.EXAMPLE
    "C:\Hyper-V\Virtual Machines\Server 2022\Virtual Hard Disks\*.vhdx" | Optimize-VirtualDisk
    Description: Optimizes the all VHDX files under "C:\Hyper-V\Virtual Machines\Server 2022\Virtual Hard Disks".
 
    Before executing, Optimize-VirtualDisk will gather a list of all VHD files used by snapshots of VMs on the system.
 
    If any VHD file targeted is used in a snapshot for a VM, Optimize-VirtualDisk will error out and will not try to optimize the VHD file as to prevent corruption.
 
.EXAMPLE
    Get-ChildItem "C:\VHDs\" -Recurse -Filter "*.vhdx" | Optimize-VirtualDisk
    Description: Optimizes the all VHDX files under "C:\VHDs\" (searched recursively).
 
    Before executing, Optimize-VirtualDisk will gather a list of all VHD files used by snapshots of VMs on the system.
 
    If any VHD file targeted is used in a snapshot for a VM, Optimize-VirtualDisk will error out and will not try to optimize the VHD file as to prevent corruption.
 
.EXAMPLE
    Get-VM -VMName "Server 2022" | Optimize-VirtualDisk
    Description: Optimizes all VHD files used by the "Server 2022" VM.
 
    The VM cannot have any snapshots.
 
    The VM must not be running and must not be in a saved state.
 
.EXAMPLE
    Get-VM | Optimize-VirtualDisk -WhatIf
    Description: Shows all VHD files that would be optimized if the command was run without -WhatIf.
 
    BE CAREFUL when running this command without -WhatIf, as it will attempt to optimize ALL VMs returned by Get-VM!
 
    For every VM returned from Get-VM, the VM must not have any snapshots, must not be running, and must not be in a saved state.
 
.LINK
    https://www.ajtek.ca/
 
.INPUTS
    Microsoft.HyperV.PowerShell.VirtualMachine.
        You can pipe virtual machines obtained from the Get-VM cmdlet to the Optimize-VirtualDisk cmdlet.
 
    System.Management.Automation.PathInfo.
        You can pipe PathInfo objects obtained from the Resolve-Path cmdlet (or other cmdlets) to the Optimize-VirtualDisk cmdlet.
 
    System.IO.FileInfo.
        You can pipe FileInfo objects obtained from the Get-Item or Get-ChildItem cmdlets (or other cmdlets) to the Optimize-VirtualDisk cmdlet.
 
    System.IO.DirectoryInfo.
        You can pipe DirectoryInfo objects obtained from the Get-Item or Get-ChildItem cmdlets (or other cmdlets) to the Optimize-VirtualDisk cmdlet. The -Filter parameter will apply to these objects when searching for files inside of them.
 
    System.String.
        You can pipe strings to the Optimize-VirtualDisk cmdlet, which will be handled the same way the -Path parameter is.
 
        Wildcard characters are supported for these paths.
 
    System.Collections.Hashtable.
        You can pipe [hashtable]s to the Optimize-VirtualDisk cmdlet.
 
        Depending on what keys are available in the hashtable, the hashtable will be handled differently (e.g.: when it contains a -Path key it will be handled as if it were passed to the -Path parameter, etc.).
 
    PSCustomObject.
        You can pipe [PSCustomObject]s to the Optimize-VirtualDisk cmdlet.
 
        Depending on what members are available in the object, the object will be handled differently (e.g.: when it contains a -Path parameter it will be handled as if it were passed to the -Path parameter, etc.).
 
.OUTPUTS
    PSCustomObject.
        The Optimize-VirtualDisk cmdlet will output [PSCustomObject]s that contains various properties, listed below.
 
    ------------- Properties -------------
 
    Path [string]
        A string representing the path to the virtual hard disk file that was optimized.
 
    OriginalSize [UInt64]
        A unsigned 64-bit int representing the size of the virtual hard disk file prior to optimization.
 
    CurrentSize [UInt64]
        A unsigned 64-bit int representing the current size of the virtual hard disk file (after optimization).
 
    Change [Int64]
        A 64-bit int representing the change in file size for the virtual hard disk file.
 
        This should be negative most of the time to indicate space was saved, however in some situations it is possible for it to be positive.
 
    ChangeFormatted [string]
        A string representing an easier-to-read version of the Change property (e.g.: -3GB when saving 3 GB of file size, 32MB when increasing file size by 32MB, etc.).
 
#>

function Optimize-VirtualDisk {
    [CmdletBinding(DefaultParameterSetName = "Path", SupportsShouldProcess)]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification = "-WhatIf support is still properly implemented via the relevant Process functions without Optimize-VirtualDisk using ShouldProcess().")]
    [OutputType([PSCustomObject])]
    param (
        [Parameter(Mandatory, Position = 0, ParameterSetName = "Path")]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Path,

        [Parameter(Mandatory, Position = 0, ParameterSetName = "VMName")]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $VMName,

        [Parameter(Mandatory, Position = 0, ParameterSetName = "VM")]
        [ValidateNotNullOrEmpty()]
        [Microsoft.HyperV.PowerShell.VirtualMachine[]]
        $VM,

        [Parameter(ParameterSetName = "Path")]
        [Parameter(ParameterSetName = "InputObject")]
        [ValidateScript({
            if ([string]::IsNullOrWhiteSpace($_)) {
                throw "Filter must not be null, empty or whitespace."
            }

            return $true
        })]
        [string]
        $Filter,

        [Parameter(ParameterSetName = "VMName")]
        [Parameter(ParameterSetName = "VM")]
        [Parameter(ParameterSetName = "InputObject")]
        [switch]
        $Shutdown,

        [Parameter()]
        [switch]
        $SkipVMCheck,

        [Parameter()]
        [switch]
        $NoLogo,

        [Parameter(Mandatory, Position = 0, ParameterSetName = "InputObject", ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [object]
        $InputObject
    )

    begin {
        # Ensure we are executing from an elevated process.
        if (!(TestElevated)) {
            Write-Error -Message "This cmdlet must be ran as an administrator." `
                -Category PermissionDenied `
                -ErrorId "CmdletRequiresElevation" `
                -RecommendedAction "Run the command again from an elevated PowerShell window."

            # Break out to avoid processing the pipeline/end block
            break
        }

        if ($NoLogo) {
            Write-Host "Brought to you by: AJ Tek Corporation"
        } else {
            Write-Host "============================="
            Write-Host "Brought to you by:"
            Write-Host "AJ Tek Corporation"
            Write-Host "https://www.ajtek.ca"
            Write-Host "============================="
        }

        $Script:VMHardDriveMap = $null

        if (!$SkipVMCheck) {
            $diskMappingTimeStart = [datetime]::UtcNow
            Write-Verbose "Generating a list of all VHDs used in VM snapshots for VMs on the local system."
            $Script:VMHardDriveMap = GetVMHardDriveMap
            $diskMappingTimeDuration = [datetime]::UtcNow - $diskMappingTimeStart
            Write-Verbose "Finished generating in $([int]$diskMappingTimeDuration.TotalMilliseconds)ms."
        }
    }

    process {
        $currentPipelineObject = $_
        $directoryFilter = "$Filter".Trim()

        if ($PSCmdlet.ParameterSetName -eq "Path") {
            foreach ($targetPath in $Path) {
                ProcessVirtualDiskPath -Path $targetPath -Filter $directoryFilter -WhatIf:$WhatIfPreference
            }
        } elseif ($PSCmdlet.ParameterSetName -eq "VMName") {
            foreach ($targetVMName in $VMName) {
                ProcessVirtualMachine -VMName $targetVMName -Shutdown:$Shutdown -WhatIf:$WhatIfPreference
            }
        } elseif ($PSCmdlet.ParameterSetName -eq "VM") {
            foreach ($targetVM in $VM) {
                ProcessVirtualMachine -VM $targetVM -Shutdown:$Shutdown -WhatIf:$WhatIfPreference
            }
        } elseif ($PSCmdlet.ParameterSetName -eq "InputObject") {
            # We cannot assume that $InputObject is going to be piped into the cmdlet, as it could also be passed as a
            # parameter. We can check to see if we are receiving piped input by checking if $_ is not $null, as it will
            # be when -InputObject is used.

            if ($null -ne $currentPipelineObject) {
                # If we have a pipeline object then $InputObject is being piped and we should process only it.
                ProcessPipelineObject -InputObject $currentPipelineObject -Filter $directoryFilter -Shutdown:$Shutdown -WhatIf:$WhatIfPreference
            } else {
                # We don't want to blindly enumerate over this object, as it's possible the user could pass a string
                # (and we would iterate over every character in it).

                if ($InputObject -is [System.Collections.IList]) {
                    Write-Verbose "`$InputObject appears to be a list of some sort (type [$($InputObject.GetType())]) - processing each item in the list."

                    foreach ($entry in $InputObject) {
                        ProcessPipelineObject -InputObject $entry -Filter $directoryFilter -Shutdown:$Shutdown -WhatIf:$WhatIfPreference
                    }
                } else {
                    ProcessPipelineObject -InputObject $InputObject -Filter $directoryFilter -Shutdown:$Shutdown -WhatIf:$WhatIfPreference
                }
            }
        } else {
            # Sanity check.
            Write-Error "Unknown parameter set name '$($PSCmdlet.ParameterSetName)' in Optimize-VirtualDisk. Please report this."
        }
    }
}

# Copyright (c) 2024 AJ Tek Corporation. All Rights Reserved.

# SIG # Begin signature block
# MIIVmwYJKoZIhvcNAQcCoIIVjDCCFYgCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUJz89fvvBidpokghlkxBBaTGo
# OYCgghH7MIIFbzCCBFegAwIBAgIQSPyTtGBVlI02p8mKidaUFjANBgkqhkiG9w0B
# AQwFADB7MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVy
# MRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEh
# MB8GA1UEAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTIxMDUyNTAwMDAw
# MFoXDTI4MTIzMTIzNTk1OVowVjELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1NlY3Rp
# Z28gTGltaXRlZDEtMCsGA1UEAxMkU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWduaW5n
# IFJvb3QgUjQ2MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjeeUEiIE
# JHQu/xYjApKKtq42haxH1CORKz7cfeIxoFFvrISR41KKteKW3tCHYySJiv/vEpM7
# fbu2ir29BX8nm2tl06UMabG8STma8W1uquSggyfamg0rUOlLW7O4ZDakfko9qXGr
# YbNzszwLDO/bM1flvjQ345cbXf0fEj2CA3bm+z9m0pQxafptszSswXp43JJQ8mTH
# qi0Eq8Nq6uAvp6fcbtfo/9ohq0C/ue4NnsbZnpnvxt4fqQx2sycgoda6/YDnAdLv
# 64IplXCN/7sVz/7RDzaiLk8ykHRGa0c1E3cFM09jLrgt4b9lpwRrGNhx+swI8m2J
# mRCxrds+LOSqGLDGBwF1Z95t6WNjHjZ/aYm+qkU+blpfj6Fby50whjDoA7NAxg0P
# OM1nqFOI+rgwZfpvx+cdsYN0aT6sxGg7seZnM5q2COCABUhA7vaCZEao9XOwBpXy
# bGWfv1VbHJxXGsd4RnxwqpQbghesh+m2yQ6BHEDWFhcp/FycGCvqRfXvvdVnTyhe
# Be6QTHrnxvTQ/PrNPjJGEyA2igTqt6oHRpwNkzoJZplYXCmjuQymMDg80EY2NXyc
# uu7D1fkKdvp+BRtAypI16dV60bV/AK6pkKrFfwGcELEW/MxuGNxvYv6mUKe4e7id
# FT/+IAx1yCJaE5UZkADpGtXChvHjjuxf9OUCAwEAAaOCARIwggEOMB8GA1UdIwQY
# MBaAFKARCiM+lvEH7OKvKe+CpX/QMKS0MB0GA1UdDgQWBBQy65Ka/zWWSC8oQEJw
# IDaRXBeF5jAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zATBgNVHSUE
# DDAKBggrBgEFBQcDAzAbBgNVHSAEFDASMAYGBFUdIAAwCAYGZ4EMAQQBMEMGA1Ud
# HwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FBQUNlcnRpZmlj
# YXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYYaHR0
# cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBDAUAA4IBAQASv6Hvi3Sa
# mES4aUa1qyQKDKSKZ7g6gb9Fin1SB6iNH04hhTmja14tIIa/ELiueTtTzbT72ES+
# BtlcY2fUQBaHRIZyKtYyFfUSg8L54V0RQGf2QidyxSPiAjgaTCDi2wH3zUZPJqJ8
# ZsBRNraJAlTH/Fj7bADu/pimLpWhDFMpH2/YGaZPnvesCepdgsaLr4CnvYFIUoQx
# 2jLsFeSmTD1sOXPUC4U5IOCFGmjhp0g4qdE2JXfBjRkWxYhMZn0vY86Y6GnfrDyo
# XZ3JHFuu2PMvdM+4fvbXg50RlmKarkUT2n/cR/vfw1Kf5gZV6Z2M8jpiUbzsJA8p
# 1FiAhORFe1rYMIIGGjCCBAKgAwIBAgIQYh1tDFIBnjuQeRUgiSEcCjANBgkqhkiG
# 9w0BAQwFADBWMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVk
# MS0wKwYDVQQDEyRTZWN0aWdvIFB1YmxpYyBDb2RlIFNpZ25pbmcgUm9vdCBSNDYw
# HhcNMjEwMzIyMDAwMDAwWhcNMzYwMzIxMjM1OTU5WjBUMQswCQYDVQQGEwJHQjEY
# MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMSswKQYDVQQDEyJTZWN0aWdvIFB1Ymxp
# YyBDb2RlIFNpZ25pbmcgQ0EgUjM2MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
# igKCAYEAmyudU/o1P45gBkNqwM/1f/bIU1MYyM7TbH78WAeVF3llMwsRHgBGRmxD
# eEDIArCS2VCoVk4Y/8j6stIkmYV5Gej4NgNjVQ4BYoDjGMwdjioXan1hlaGFt4Wk
# 9vT0k2oWJMJjL9G//N523hAm4jF4UjrW2pvv9+hdPX8tbbAfI3v0VdJiJPFy/7Xw
# iunD7mBxNtecM6ytIdUlh08T2z7mJEXZD9OWcJkZk5wDuf2q52PN43jc4T9OkoXZ
# 0arWZVeffvMr/iiIROSCzKoDmWABDRzV/UiQ5vqsaeFaqQdzFf4ed8peNWh1OaZX
# nYvZQgWx/SXiJDRSAolRzZEZquE6cbcH747FHncs/Kzcn0Ccv2jrOW+LPmnOyB+t
# AfiWu01TPhCr9VrkxsHC5qFNxaThTG5j4/Kc+ODD2dX/fmBECELcvzUHf9shoFvr
# n35XGf2RPaNTO2uSZ6n9otv7jElspkfK9qEATHZcodp+R4q2OIypxR//YEb3fkDn
# 3UayWW9bAgMBAAGjggFkMIIBYDAfBgNVHSMEGDAWgBQy65Ka/zWWSC8oQEJwIDaR
# XBeF5jAdBgNVHQ4EFgQUDyrLIIcouOxvSK4rVKYpqhekzQwwDgYDVR0PAQH/BAQD
# AgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwGwYD
# VR0gBBQwEjAGBgRVHSAAMAgGBmeBDAEEATBLBgNVHR8ERDBCMECgPqA8hjpodHRw
# Oi8vY3JsLnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNDb2RlU2lnbmluZ1Jvb3RS
# NDYuY3JsMHsGCCsGAQUFBwEBBG8wbTBGBggrBgEFBQcwAoY6aHR0cDovL2NydC5z
# ZWN0aWdvLmNvbS9TZWN0aWdvUHVibGljQ29kZVNpZ25pbmdSb290UjQ2LnA3YzAj
# BggrBgEFBQcwAYYXaHR0cDovL29jc3Auc2VjdGlnby5jb20wDQYJKoZIhvcNAQEM
# BQADggIBAAb/guF3YzZue6EVIJsT/wT+mHVEYcNWlXHRkT+FoetAQLHI1uBy/YXK
# ZDk8+Y1LoNqHrp22AKMGxQtgCivnDHFyAQ9GXTmlk7MjcgQbDCx6mn7yIawsppWk
# vfPkKaAQsiqaT9DnMWBHVNIabGqgQSGTrQWo43MOfsPynhbz2Hyxf5XWKZpRvr3d
# MapandPfYgoZ8iDL2OR3sYztgJrbG6VZ9DoTXFm1g0Rf97Aaen1l4c+w3DC+IkwF
# kvjFV3jS49ZSc4lShKK6BrPTJYs4NG1DGzmpToTnwoqZ8fAmi2XlZnuchC4NPSZa
# PATHvNIzt+z1PHo35D/f7j2pO1S8BCysQDHCbM5Mnomnq5aYcKCsdbh0czchOm8b
# kinLrYrKpii+Tk7pwL7TjRKLXkomm5D1Umds++pip8wH2cQpf93at3VDcOK4N7Ew
# oIJB0kak6pSzEu4I64U6gZs7tS/dGNSljf2OSSnRr7KWzq03zl8l75jy+hOds9TW
# SenLbjBQUGR96cFr6lEUfAIEHVC1L68Y1GGxx4/eRI82ut83axHMViw1+sVpbPxg
# 51Tbnio1lB93079WPFnYaOvfGAA0e0zcfF/M9gXr+korwQTh2Prqooq2bYNMvUoU
# KD85gnJ+t0smrWrb8dee2CvYZXD5laGtaAxOfy/VKNmwuWuAh9kcMIIGZjCCBM6g
# AwIBAgIRAMyLU7NDPs1zQXTEeYo/k2YwDQYJKoZIhvcNAQEMBQAwVDELMAkGA1UE
# BhMCR0IxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDErMCkGA1UEAxMiU2VjdGln
# byBQdWJsaWMgQ29kZSBTaWduaW5nIENBIFIzNjAeFw0yMjA2MzAwMDAwMDBaFw0y
# NTA2MjkyMzU5NTlaMFkxCzAJBgNVBAYTAkNBMRAwDgYDVQQIDAdPbnRhcmlvMRsw
# GQYDVQQKDBJBSiBUZWsgQ29ycG9yYXRpb24xGzAZBgNVBAMMEkFKIFRlayBDb3Jw
# b3JhdGlvbjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAOLV0fXklSI7
# zs4Awbc5rzQGG/XJaEgZLEXFVvzqKWlApLYSdXMLTzW14/IEoVMmi+qunmgIF98c
# 3gUFm6dTSkf+ccSIWgHwjD/pPMX4leIO1Qen+OBup4p33XRNqHsWLzBqlWmKRcLW
# 5cyRDRXedgZDvZozhxTqLhk0tFdTcBoGULA13Z/TfotbwNCx05W304VgluUogNX2
# 4yv80QYmHa6667ewXuAPNiPWQgFE4fyXOvFsRUFEtllRZmUpOPqZtGURw2ZYd+cZ
# UFn4QrC9DzlESFEjQzMMF5iUjrKCI25jq53MkTZHnOjO9KGH5SvcLo5CbgNptpzr
# d1P7AsoHFedvErNbp6YOhKrkv7F0ksVgqBh1v2Yc1ShpzZBEJ6pL+QsdnWzl6Wzb
# B8xEYEdbNF0A0/kyFHm2tYSxxCsVO4caoL2CcsA3q/1h73MBWEcNT2fV6o25L+LY
# sNUkFRc6ZRqOQ/ub9na8jCOwuhxi8MXTGo22Crinp5bbzYA/liNm97G77mJe59za
# aaoulTWzk5y06k7VgLDYtR7IgZ47Aytnj/jr7S5/P/1F2K3lbRUdGyKdifubSgjk
# m1gubK0cm3Y6L7Ar2X1nBm+PpAZqA54jQBs9cV03MVDjZb3AvjooCi9uOCJ4998U
# TV9Sn2873/GUXE6dRB+w0fD/FcUxX2pjAgMBAAGjggGsMIIBqDAfBgNVHSMEGDAW
# gBQPKssghyi47G9IritUpimqF6TNDDAdBgNVHQ4EFgQU0JE9KqcvG1A1pTeA+vcd
# t1flqK0wDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYI
# KwYBBQUHAwMwSgYDVR0gBEMwQTA1BgwrBgEEAbIxAQIBAwIwJTAjBggrBgEFBQcC
# ARYXaHR0cHM6Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EMAQQBMEkGA1UdHwRCMEAw
# PqA8oDqGOGh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY0NvZGVT
# aWduaW5nQ0FSMzYuY3JsMHkGCCsGAQUFBwEBBG0wazBEBggrBgEFBQcwAoY4aHR0
# cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUHVibGljQ29kZVNpZ25pbmdDQVIz
# Ni5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29tMCEGA1Ud
# EQQaMBiBFmFqLmFjY291bnRpbmdAYWp0ZWsuY2EwDQYJKoZIhvcNAQEMBQADggGB
# ACa/QJzMPVDohIgQtL4/Yr7z7DlaybhjLQ2gEx7pU6G/hNQ+kClqLR3hykkJ7fwZ
# 4cYX1TF4JWnkmQ0GZwj5bk5RaSyDatQVVIW8AQNMIEEhUftHJn1REz16CzXxlgzd
# d+GTqYmwommBa6DlFO88fwF0FL3KJgvNguQGSU9sGIGUWyQuxqFUwXRgsXpwQbpR
# 1H6qsViLN1SGPXO+iqUSyejmd9mIbc8b0IxuR6rDtU2PIcU8XzwsJv8M/L1paQut
# 4m7dNw54gsoRVp+KJWawkkEVM6xvszCo8VIk8ZGetRCBT+ZSunIb/LFripb++lR8
# tIBkq8zYEhEp9U8GQZbKNZzfelix4kRt5wUq39rpBU8aHoU4GRXFs571jb/qBz/x
# AVckN5cosppJxe+AW/TR9qKrL8uKOJ9cCLRXjPdLGSmHA5XMN6ecgRE+yfLMOauX
# pUo33dCGh36TZuSi7P4uz2trEEUfmaQlr/TEp0xtbgUxTopdUYh9xagN4bbkRmoB
# izGCAwowggMGAgEBMGkwVDELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1NlY3RpZ28g
# TGltaXRlZDErMCkGA1UEAxMiU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWduaW5nIENB
# IFIzNgIRAMyLU7NDPs1zQXTEeYo/k2YwCQYFKw4DAhoFAKB4MBgGCisGAQQBgjcC
# AQwxCjAIoAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYB
# BAGCNwIBCzEOMAwGCisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYEFKRhfcdcIXwh
# eAl/HjqC9bsK/pbYMA0GCSqGSIb3DQEBAQUABIICAFhXWN/oangtkDYzYk0S4rFE
# 7TpHA5wWV1h1SvstxGWu5qv3Hil1KMlHIFYYZ1pQziVX97bK4nlkm3lg5elnsdvT
# KHbV97hmK8z93h6MH8edeHc8w8phDZKceR8PcA7uIafPT5pAYxF/F2TWgFnuSauw
# V40+3CYEY5JSsB/q9bUeFdTsf76vzgxeEfpXjv5Pq+ubCQyb2NPPVSEPJ4Xg9wB2
# a78y3XQ5YBSKXgJ8xzXNyZkshIH/HJA+Txvy2UxO5gxyWyrzDv/RmARKEgU6wN9d
# Z2AWVSFvqkyXR6JWx5BVYb88mNtHeoz5tFdvN1oriYYXCvfkcAkgoRoZxPuTAPG4
# n4a1IR0UkQtdz+besynaqVv0eefW2tiqnZfntDqNpQlPz8ccTOvntyUtaeFh4EcV
# JKVj4s7WjbSdZaRDv7KI4MDxcD1Sds2P71hCPgnh3BmoVpKvcm93AsTiBBQFbjSy
# +rjFOfmSww9ZhIbYydm7rJ3HeoqabjYRph399odCqyyw54UJc0H5DC3Ogb84Rbtk
# D2UrxXphaAHfPoCdAhoLXBhxsaNdJyY/pTK8/6sMzEFJb7uCA9ZMg9ldEnCYfXOY
# NRSXbxaIlm8vQvPwHcZs2WHUB3kVQwcKvUO5djRHmi9Xvuxb4BRoTbO9uiL0k5AV
# firxrpIfV0cWybk8MdWC
# SIG # End signature block