ozo-ad-lab-create-vms.ps1

#Requires -Modules @{ModuleName="OZO";ModuleVersion="1.5.0"},@{ModuleName="OZOLogger";ModuleVersion="1.1.0"} -RunAsAdministrator

<#PSScriptInfo
    .VERSION 1.0.0
    .GUID e2071482-27a4-415c-b3db-43d5351d24bb
    .AUTHOR Andy Lievertz <alievertz@onezeroone.dev>
    .COMPANYNAME One Zero One
    .COPYRIGHT This script is licensed under the GNU Public License ("GPL") version 2.0.
    .TAGS
    .LICENSEURI https://github.com/onezeroone-dev/OZO-AD-Lab-Create-VMs/blob/main/LICENSE
    .PROJECTURI https://github.com/onezeroone-dev/OZO-AD-Lab-Create-VMs
    .ICONURI
    .EXTERNALMODULEDEPENDENCIES
    .REQUIREDSCRIPTS
    .EXTERNALSCRIPTDEPENDENCIES
    .RELEASENOTES https://github.com/onezeroone-dev/OZO-AD-Lab-Create-VMs/blob/main/CHANGELOG.md
    .PRIVATEDATA
#>


<#
    .DESCRIPTION
    An interactive script that creates a the Hyper-V virtual machines required for the One Zero One AD Lab.
    .PARAMETER ClientISO
    The path to the customized Client ISO. Defaults to C:\ozo-ad-lab\ISO\AD-Lab-Client.iso.
    .PARAMETER DCISO
    The path to the customized DC ISO. Defaults to C:\ozo-ad-lab\ISO\AD-Lab-DC.iso.
    .PARAMETER LaofISO
    The path to the Windows 11 Lanuages & Optional Features ISO. Defaults to C:\ozo-ad-lab\ISO\Windows-11-Lanuages-and-Optional-Features.iso.
    .PARAMETER RouterISO
    The path to the customized Router ISO. Defaults to C:\ozo-ad-lab\ISO\AD-Lab-Router.iso.
    .PARAMETER ServerISO
    The path for the VHDX files. Defaults to $Env:ProgramData\Microsoft\Windows\Virtual Hard Disks.
    .EXAMPLE
    ozo-ad-lab-create-vms
    .EXAMPLE
    $isoPath = (Join-Path -Path $Env:USERPROFILE -ChildPath "Downloads")
    ozo-ad-lab-create-vms -ClientISO "$isoPath\AD-Lab-Client.iso" -DCISO "$isoPath\AD-Lab-DC.iso"
    .LINK
    https://github.com/onezeroone-dev/OZO-AD-Lab-Create-VMs/blob/main/README.md
    .NOTES
    Run this script in an Administrator PowerShell.
#>
 
Param(
    [Parameter(Mandatory=$false,HelpMessage="The path to the Client ISO")][String]$ClientISO = "C:\ozo-ad-lab\ISO\AD-Lab-Client.iso",
    [Parameter(Mandatory=$false,HelpMessage="The path to the DC ISO")][String]$DCISO = "C:\ozo-ad-lab\ISO\AD-Lab-DC.iso",
    [Parameter(Mandatory=$false,HelpMessage="The path to the Lanugages & Optional Features ISO")][String]$LaofISO = "C:\ozo-ad-lab\ISO\Windows-11-Lanuages-and-Optional-Features.iso",
    [Parameter(Mandatory=$false,HelpMessage="The path to the Router ISO")][String]$RouterISO = "C:\ozo-ad-lab\ISO\AD-Lab-Router.iso",
    [Parameter(Mandatory=$false,HelpMessage="The path to the Server ISO")][String]$ServerISO = "C:\ozo-ad-lab\ISO\AD-Lab-Server.iso",
    [Parameter(Mandatory=$false,HelpMessage="The path for VHDX files")][String]$VHDXPath = (Join-Path -Path $Env:ProgramData -ChildPath "Microsoft\Windows\Virtual Hard Disks")
)

# CLASSES
Class ADLCVM {
    # PROPERTIES: Arrays, Strings
    [Array]  $hyperVSwitches = @("AD Lab External","AD Lab Private")
    [String] $clientISO      = $null
    [String] $dcISO          = $null
    [String] $laofISO        = $null
    [String] $routerISO      = $null
    [String] $serverISO      = $null
    [String] $vhdxPath       = $null
    # PROPERTIES: PSCustomObjects
    [PSCustomObject] $ozoLogger = @{}
    # PROPERTIES: Lists
    [System.Collections.Generic.List[PSCustomObject]] $ozoVMs = @()
    # METHODS
    # Constructor method
    ADLCVM($ClientISO,$DCISO,$LaofISO,$RouterISO,$ServerISO,$VHDXPath) {
        # Set properties
        $this.clientISO = $clientISO
        $this.dcISO     = $DCISO
        $this.laofISO   = $LaofISO
        $this.routerISO = $RouterISO
        $this.serverISO = $ServerISO
        $this.vhdxPath  = $VHDXPath
        $this.ozoLogger = (New-OZOLogger)
        # Announce ourselves to the world.
        $this.ozoLogger.Write("Process starting.","Information")
        $this.ozoLogger
        # Call validate configuration and validate environment to determine if we can proceed
        If (($this.ValidateConfiguration() -And $this.ValidateEnvironment()) -eq $true) {
            # Report
            $this.ozoLogger.Write("Configuration and environment validated; evaluating VMs.","Information")
            # Create the virtual machine objects
            $this.ozoVMs.Add(([ADLCVMVirtualMachine]::new("AD Lab 01 router.contoso.com","Linux",1,1073741824,68719476736,$this.vhdxPath,$this.routerISO,"AD Lab External","AD Lab Private")))
            # Creat an ADLCVMVirtualMachine object for the DC
            $this.ozoVMs.Add(([ADLCVMVirtualMachine]::new("AD Lab 02 dc.contoso.com","Windows",1,2147483648,137438953472,$this.vhdxPath,$this.dcISO,"AD Lab Private",$null)))
            # Creat an ADLCVMVirtualMachine object for the Server
            $this.ozoVMs.Add(([ADLCVMVirtualMachine]::new("AD Lab 03 server.contoso.com","Windows",1,2147483648,137438953472,$this.vhdxPath,$this.serverISO,"AD Lab Private",$null)))
            # Creat an ADLCVMVirtualMachine object for the Client
            $this.ozoVMs.Add(([ADLCVMVirtualMachine]::new("AD Lab 04 client.contoso.com","Windows",2,4294967296,137438953472,$this.vhdxPath,$this.clientISO,"AD Lab Private",$null)))
            # Configuration and environment validate; iterate through the VM objects
            ForEach ($VM in $this.ozoVMs) {
                If ($VM.Create -eq $true) {
                    # VM validated; call the CreateVM method to create the VM and set Created on the VM object
                    If ($VM.CreateVM() -eq $true) {
                        # VM was created
                        $this.ozoLogger.Write(("Creating the " + $VM.vmName + " VM."),"Information")
                    } Else {
                        # VM was not created
                        $this.ozoLogger.Write(("Error creating the " + $VM.vmName + " virtual machine for the following reasons:`r`n" + ($VM.Messages -Join("`r`n"))),"Warning")
                    }
                } Else {
                    # VM did not validate; report
                    $this.ozoLogger.Write(("Skipping the " + $VM.vmName + " virtual machine for the following reasons:`r`n" + ($VM.Messages -Join("`r`n"))),"Warning")
                }
                
            }
        } Else {
            $this.ozoLogger.Write("Configuration and/or environment did not validate.","Error")
        }
        # Bid the world adieu.
        $this.ozoLogger.Write("Process complete.","Information")
    }
    # Validate configuration method
    Hidden [Boolean] ValidateConfiguration() {
        # Control variable
        [Boolean] $Return = $true
        ForEach ($Path in $this.clientISO,$this.dcISO,$this.laofISO,$this.serverISO) {
            # Determine if the path is invalid
            If ((Test-Path -Path $Path) -eq $false) {
                # Path is invalid
                $this.ozoLogger.Write(("Cannot find " + $Path),"Error")
                $Return = $false
            }
        }
        # Return
        return $Return
    }
    # Validate environment methods
    Hidden [Boolean] ValidateEnvironment() {
        # Control variable
        [Boolean] $Return = $true
        # Test if session is user-interactive
        If ((Get-OZOUserInteractive) -eq $false) {
            # Session is not user-interactive
            $this.ozoLogger.Write("Please run this script in a user-interactive session.","Error")
            $Return = $false
        }
        # Determine if current user is an Administrator or a member of Hyper-V Administrators
        If ((Test-OZOLocalAdministrator) -eq $false -And (Test-OZOHyperVAdministrator) -eq $false) {
            # User is neither an Administrator or a Hyper-V Administrator
            $this.ozoLogger.Write("User is not an Administrator or a member of Hyper-V Administrators. Run this script in an Administrator PowerShell session or add your user to the local Hyper-V Administrators group.")
            $Return = $false
        }
        # Iterate through the Hyper-V virtual switches
        ForEach ($virtualSwitch in $this.hyperVSwitches) {
            # Determine if the switch is not present
            If ((Get-VMSwitch).Name -NotContains $virtualSwitch) {
                # Switch is not present
                $this.ozoLogger.Write(("Hyper-V virtual switch " + $virtualSwitch + " not found."),"Error")
                $Return = $false
            }
        }
        # Determine if the VHDXPath is not writable
        If ((Test-OZOPath -Writable -Path $this.vhdxPath) -eq $false) {
            # Path is not writable
            $this.ozoLogger.Write(("The VHDX path is not writable."),"Error")
            $Return = $false
        }
        # Determine if the Hyper-V Feature is not installed
        If ([Boolean](Get-WindowsOptionalFeature -Online -FeatureName "Microsoft-Hyper-V") -eq $false) {
            # Feature is not present
            $this.ozoLogger.Write(("The Hyper-V Feature is not installed. Please see https://onezeroone.dev/active-directory-lab-prerequisites."),"Error")
            $Return = $false
        }
        # Return
        return $Return
    }
}

Class ADLCVMVirtualMachine {
    # PROPERTIES: Arrays, Booleans, Ints, Strings
    [Array]   $osList   = @("Linux","Windows")
    [Boolean] $Create   = $true
    [Int16]   $vmVCPUs  = $null
    [Int64]   $vmDisk   = $null
    [Int64]   $vmMemory = $null
    [String]  $vhdxPath = $null
    [String]  $vmISO    = $null
    [String]  $vmName   = $null
    [String]  $vmOS     = $null
    [String]  $vmSBT    = $null
    [String]  $vmSwitch = $null
    [String]  $vmSwit2h = $null    
    # Properties: Lists
    [System.Collections.Generic.List[String]] $Messages = @()
    # METHODS
    # Constructor method
    ADLCVMVirtualMachine($Name,$OS,$vCPUs,$Memory,$Disk,$VHDXPath,$ISO,$Network,$Netwo2k) {
        # Set properties
        $this.vmName   = $Name
        $this.vmOS     = $OS
        $this.vmVCPUs  = $vCPUs
        $this.vmMemory = $Memory
        $this.vmDisk   = $Disk
        $this.vhdxPath = (Join-Path -Path $VHDXPath -ChildPath ($this.vmName + ".vhdx"))
        $this.vmISO    = $ISO
        $this.vmSwitch = $Network
        $this.vmSwit2h = $Netwo2k
        # Switch on OS to set vmSBT
        Switch($this.vmOS) {
            "Linux" {
                $this.vmSBT = "MicrosoftUEFICertificateAuthority"
            }
            default {
                $this.vmSBT = "MicrosoftWindows"
            }
        }
        # Call ValidateVM to set Create
        $this.Create = $this.ValidateVM()
    }
    # Validate VM method
    Hidden [Boolean] ValidateVM() {
        # Control variable
        [Boolean] $Return = $true
        # Determine if the VM already exists
        If ((Get-VM).Name -Contains $this.vmName) {
            # VM exists; skipping
            $this.Messages.Add("VM already exists.")
            $Return = $false
        }
        If ((Test-Path -Path $this.vhdxPath) -eq $true) {
            # VM disk exists; skipping
            $this.Messages.Add("VM disk already exists.")
            $Return = $false
        }
        # Determine if OS is valid
        If ($this.osList -NotContains $this.vmOS) {
            # OS is not handled
            $this.Messages.Add(("Operating system " + $this.vmOS + " is invalid."))
        }
        # Return
        return $Return
    }
    # Create VM method
    [Boolean] CreateVM() {
        # Control variables
        [Boolean] $Return = $true
        # Create the VM
        Try {
            New-VM -Name $this.vmName -Generation 2 -MemoryStartupBytes $this.vmMemory -SwitchName $this.vmSwitch -NewVHDPath $this.vhdxPath -NewVHDSizeBytes $this.vmDisk -ErrorAction Stop
            # Success; configure processor count, automagic start action, and automagic stop action; and disable checkpoints
            Set-VM -Name $this.vmName -ProcessorCount $this.vmVCPUs -AutomaticStartAction "Nothing" -AutomaticStopAction "Shutdown" -CheckpointType "Disabled"
            # Configure Memory and Memory Buffer
            Set-VMMemory -VMName $this.vmName -DynamicMemoryEnabled $true -MinimumBytes 536870912 -MaximumBytes $this.vmMemory -Buffer 5 -ErrorAction Stop
            # Configure firmware
            Set-VMFirmware -VMName $this.vmName -EnableSecureBoot "On" -SecureBootTemplate $this.vmSBT -ErrorAction Stop
            # Enable TPM and Key Protector
            Set-VMKeyProtector -VMName $this.vmName -NewLocalKeyProtector
            Enable-VMTPM -VMName $this.vmName -ErrorAction Stop
            # Configure the DVD drive
            Add-VMDvdDrive -VMName $this.vmName -Path $this.vmISO
            # Set DVD drive as first boot option
            Set-VMFirmware -VMName $this.vmName -FirstBootDevice (Get-VMDvdDrive -VMName $this.vmName)
            # Configure integrations
            Enable-VMIntegrationService -VMName $this.vmName -Name "Guest Service Interface","Heartbeat","Key-Value Pair Exchange","Shutdown","Time Synchronization","VSS" -ErrorAction Stop
            # Determine if this VM should ahve a second vSwitch
            If ([String]::IsNullOrEmpty($this.vmSwit2h) -eq $false) {
                # Add second vSwitch
                Add-VMNetworkAdapter -VMName $this.vmName -SwitchName $this.vmSwit2h -ErrorAction Stop
            }
        } Catch {
            # Failure
            $this.Messages.Add(("Error creating or configuring " + $this.vmName + " VM. Error message is " + $_ + "`r`nDeleting."))
            $Return = $false
            # Determine if the VM was created in any capacity or configuration
            If ([Boolean](Get-VM -VMName $this.vmName) -eq $true) {
                # VM was created; try to remove it
                Try {
                    Remove-VM -VMName $this.vmName -Force -ErrorAction Stop
                    # Success; determine if a VHDX was created
                    If ((Test-Path -Path $this.vhdxPath) -eq $true) {
                        # VHDX was created; remove it
                        Remove-Item -Path $this.vhdxPath -Force -ErrorAction Stop
                    }
                } Catch {
                    # Failure
                    $this.Messages.Add(("Unable to delete the " + $this.vmName + " VM. AD Lab configuration may be in an inconsistent state."))
                }
            }
        }

        # Return
        return $Return
    }
}

# FUNCTIONS
Function Test-OZOHyperVAdministrator {
    # Determine if current user is a member of Administrators or Hyper-V Administrators
    If ((([ADSI]"WinNT://./Hyper-V Administrators,group").psbase.Invoke("Members") | ForEach-Object {$_.GetType().InvokeMember("Name",'GetProperty', $null, $_, $null)}) -Contains [System.Environment]::UserName) {
        # User is a member of the Hyper-V Administrators group
        return $true
    } Else {
        # User not a member of the Hyper-V Administrators group
        return $false
    }
}

# MAIN
[ADLCVM]::new($ClientISO,$DCISO,$LaofISO,$RouterISO,$ServerISO,$VHDXPath) | Out-Null

# SIG # Begin signature block
# MIIfcgYJKoZIhvcNAQcCoIIfYzCCH18CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDJ+Ln7c3heY8/S
# xEc4lBw43Yfu+3QHJO42J2TePVrtC6CCDPgwggZyMIIEWqADAgECAghkM1HTxzif
# CDANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx
# EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8G
# A1UEAwwoU1NMLmNvbSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTAe
# Fw0xNjA2MjQyMDQ0MzBaFw0zMTA2MjQyMDQ0MzBaMHgxCzAJBgNVBAYTAlVTMQ4w
# DAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjERMA8GA1UECgwIU1NMIENv
# cnAxNDAyBgNVBAMMK1NTTC5jb20gQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBD
# QSBSU0EgUjEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCfgxNzqrDG
# bSHL24t6h3TQcdyOl3Ka5LuINLTdgAPGL0WkdJq/Hg9Q6p5tePOf+lEmqT2d0bKU
# Vz77OYkbkStW72fL5gvjDjmMxjX0jD3dJekBrBdCfVgWQNz51ShEHZVkMGE6ZPKX
# 13NMfXsjAm3zdetVPW+qLcSvvnSsXf5qtvzqXHnpD0OctVIFD+8+sbGP0EmtpuNC
# GVQ/8y8Ooct8/hP5IznaJRy4PgBKOm8yMDdkHseudQfYVdIYyQ6KvKNc8HwKp4WB
# wg6vj5lc02AlvINaaRwlE81y9eucgJvcLGfE3ckJmNVz68Qho+Uyjj4vUpjGYDdk
# jLJvSlRyGMwnh/rNdaJjIUy1PWT9K6abVa8mTGC0uVz+q0O9rdATZlAfC9KJpv/X
# gAbxwxECMzNhF/dWH44vO2jnFfF3VkopngPawismYTJboFblSSmNNqf1x1KiVgMg
# Lzh4gL32Bq5BNMuURb2bx4kYHwu6/6muakCZE93vUN8BuvIE1tAx3zQ4XldbyDge
# VtSsSKbt//m4wTvtwiS+RGCnd83VPZhZtEPqqmB9zcLlL/Hr9dQg1Zc0bl0EawUR
# 0tOSjAknRO1PNTFGfnQZBWLsiePqI3CY5NEv1IoTGEaTZeVYc9NMPSd6Ij/D+KNV
# t/nmh4LsRR7Fbjp8sU65q2j3m2PVkUG8qQIDAQABo4H7MIH4MA8GA1UdEwEB/wQF
# MAMBAf8wHwYDVR0jBBgwFoAU3QQJB6L1en1SUxKSle44gCUNplkwMAYIKwYBBQUH
# AQEEJDAiMCAGCCsGAQUFBzABhhRodHRwOi8vb2NzcHMuc3NsLmNvbTARBgNVHSAE
# CjAIMAYGBFUdIAAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwOwYDVR0fBDQwMjAwoC6g
# LIYqaHR0cDovL2NybHMuc3NsLmNvbS9zc2wuY29tLXJzYS1Sb290Q0EuY3JsMB0G
# A1UdDgQWBBRUwv4QlQCTzWr158DX2bJLuI8M4zAOBgNVHQ8BAf8EBAMCAYYwDQYJ
# KoZIhvcNAQELBQADggIBAPUPJodwr5miyvXWyfCNZj05gtOII9iCv49UhCe204MH
# 154niU2EjlTRIO5gQ9tXQjzHsJX2vszqoz2OTwbGK1mGf+tzG8rlQCbgPW/M9r1x
# xs19DiBAOdYF0q+UCL9/wlG3K7V7gyHwY9rlnOFpLnUdTsthHvWlM98CnRXZ7WmT
# V7pGRS6AvGW+5xI+3kf/kJwQrfZWsqTU+tb8LryXIbN2g9KR+gZQ0bGAKID+260P
# Z+34fdzZcFt6umi1s0pmF4/n8OdX3Wn+vF7h1YyfE7uVmhX7eSuF1W0+Z0duGwdc
# +1RFDxYRLhHDsLy1bhwzV5Qe/kI0Ro4xUE7bM1eV+jjk5hLbq1guRbfZIsr0WkdJ
# LCjoT4xCPGRo6eZDrBmRqccTgl/8cQo3t51Qezxd96JSgjXktefTCm9r/o35pNfV
# HUvnfWII+NnXrJlJ27WEQRQu9i5gl1NLmv7xiHp0up516eDap8nMLDt7TAp4z5T3
# NmC2gzyKVMtODWgqlBF1JhTqIDfM63kXdlV4cW3iSTgzN9vkbFnHI2LmvM4uVEv9
# XgMqyN0eS3FE0HU+MWJliymm7STheh2ENH+kF3y0rH0/NVjLw78a3Z9UVm1F5VPz
# iIorMaPKPlDRADTsJwjDZ8Zc6Gi/zy4WZbg8Zv87spWrmo2dzJTw7XhQf+xkR6Od
# MIIGfjCCBGagAwIBAgIQZ2iSsNbwOsjnLExSAX6F6DANBgkqhkiG9w0BAQsFADB4
# MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0b24x
# ETAPBgNVBAoMCFNTTCBDb3JwMTQwMgYDVQQDDCtTU0wuY29tIENvZGUgU2lnbmlu
# ZyBJbnRlcm1lZGlhdGUgQ0EgUlNBIFIxMB4XDTI0MTExNjEwMzUyOFoXDTI1MTEx
# NjEwMzUyOFowZTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCENvbG9yYWRvMQ8wDQYD
# VQQHDAZEZW52ZXIxGDAWBgNVBAoMD0FuZHJldyBMaWV2ZXJ0ejEYMBYGA1UEAwwP
# QW5kcmV3IExpZXZlcnR6MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA
# vIBAQzK0aahepOrPmvCEqfd6dMZC4GvV7kflKwrn4QPJGfqhFmUtadP1e3ange8O
# QZ3/w7UjOTAUNUHfhjbSgUBlKjbS6EWQKZuRFzI3SNkMJkcjTX4uS2P4QsnwM+SW
# IE5me3CTssdjtgue+Iiy53TMgW8JpoxiULVxmm3bhCRUAgxWeT6tzjytR1UyGcMc
# cm/YE6TOgsCHiZoo4X4HJD9iHDrNldArq04Jl6FsADxEswttKyfqpIRJLoAysVl1
# f8CEDBwhszJrEXBnAlWViJFfNY+dKP4jhf7lLqSvPCuADqP2jvM0Ym5I8qDGMz9j
# XPSMLF58MFB4vM4viS7nLRFJ8S1Q98vQvB8W4kk0WPuiZbZTHsROzohE1VSbLnIY
# ag5dDOWI8L6yutAsfdZFYFmSTKcMSiOj5VbK4LhAJUL2G8vPwpTGFgr+cEp0p62F
# P0WXK+/cRfGqodI5S+bg+9rQTD9zf829DwraSRAt5P5zrQk4WPst3JW/vIKNx7cV
# AgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFFTC/hCVAJPN
# avXnwNfZsku4jwzjMHoGCCsGAQUFBwEBBG4wbDBIBggrBgEFBQcwAoY8aHR0cDov
# L2NlcnQuc3NsLmNvbS9TU0xjb20tU3ViQ0EtQ29kZVNpZ25pbmctUlNBLTQwOTYt
# UjEuY2VyMCAGCCsGAQUFBzABhhRodHRwOi8vb2NzcHMuc3NsLmNvbTBRBgNVHSAE
# SjBIMAgGBmeBDAEEATA8BgwrBgEEAYKpMAEDAwEwLDAqBggrBgEFBQcCARYeaHR0
# cHM6Ly93d3cuc3NsLmNvbS9yZXBvc2l0b3J5MBMGA1UdJQQMMAoGCCsGAQUFBwMD
# ME0GA1UdHwRGMEQwQqBAoD6GPGh0dHA6Ly9jcmxzLnNzbC5jb20vU1NMY29tLVN1
# YkNBLUNvZGVTaWduaW5nLVJTQS00MDk2LVIxLmNybDAdBgNVHQ4EFgQUSj8HrSK7
# f/j+Dz31jJFhOF7rJUMwDgYDVR0PAQH/BAQDAgeAMA0GCSqGSIb3DQEBCwUAA4IC
# AQBf4lcc6FUJ1W/opNz8yjS9qLUy9cQt0s35BhasB5QoTbDaW4jv9xnFGhQVg6n+
# jhL0i94Vsywd/MRBb8lYGpuBZnS/7LHuRZu7qUuud+IMDyRHIyBK6koN5bfyA5VY
# c7bFbNpbe1s1hMWke8di4qgMLZKDfyG/RtA0swf5t4UgQLPP0h+koZ8X8V5+P0V0
# 1HsdXyXd+ojo38EoZyCKfQL2aAwMPwzZfCbmI5SRXNOc6K8oqXzQcendhlKSfVBo
# Zgpi+1updqbD4jmJfYdK5AYPxJ3YH6td6ETtr8owL+bmX8lQjlXPOwVnC11rVlNB
# VjqtaJRUClLtiNiYSTKVfjdmGVJ4+sNov0dWhHc0A9o5NX/05VVYTlImuJpnG5Og
# o7w6kWRdsgE8gM58jWf7XfI6aQS0Np/z2B+ZBj0K93khEHBX7cvvORa92LCHiVeP
# km+zEAMXgxIPs/e8cmcc/o3CORgzEwxlH9Z3UOWCuXSHD3P2RPNDAY+WPdjSHm9f
# JFlGq+f9iKyedxYa/NNjNag/5EbZ+Z2NldtSMNeFdsejGJ/TJHF1PyJd4aXx9J1i
# B/IZBOoJYyh9xpQ3ljZUKE/4otPi7INpuDFwgWiUHZZJVvrGTWwxH1Yhf8P+VpFf
# aNqsBuvklUcUDs3RNE0f1qlgFfcnAepFF+RiBRqmsj29fjGCEdAwghHMAgEBMIGM
# MHgxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3Rv
# bjERMA8GA1UECgwIU1NMIENvcnAxNDAyBgNVBAMMK1NTTC5jb20gQ29kZSBTaWdu
# aW5nIEludGVybWVkaWF0ZSBDQSBSU0EgUjECEGdokrDW8DrI5yxMUgF+hegwDQYJ
# YIZIAWUDBAIBBQCgfDAQBgorBgEEAYI3AgEMMQIwADAZBgkqhkiG9w0BCQMxDAYK
# KwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG
# 9w0BCQQxIgQgfgl5UDls+n69WhG6xwN7dh1WPghsW3LS8VeCDZocQKwwDQYJKoZI
# hvcNAQEBBQAEggGAWciUP5IKAQUrcOx807VGnFmwhTk2yMREZ4vdn2xzPw+r2lO8
# XdDYvDQX0gpgDJmJRtf58dpINA1aZhNUsctS3LaPz4FB+HPkuJ9UkUzNz1BfymEH
# 2arR3jb0KfnU0vXHcByZV+9UK9D0ZZDSX13zVT/MqfpyGW7fQHhFWOsZzfcnEUsY
# SEUqL5bFAIfhbzRIqwoL7JIhtogQdjEnsvOFJaPZCOqHVuTNBSC3tfHU4n2b25dQ
# +6i49ELlaq8PLDTjdbMIqXoxvtkYZE7dsTzx9QRB8ZKW+As3YOX7KNis5b7AgbgL
# wMlfqpd3gUl5IcbpMdfBOY3XUr3JBifnMWj0V9qAaq8kKm/lQOzdtEylXh+Md1t+
# mC47V3ncO64BwEstvgXUqCi/Ht/q/xW5PNGwafsnbv+ZazbV27oxs85CKMMkoz4V
# 8mDNpoYf9cTHEUsXr4PygQD8GJTgRndbnZ73oXW0nE5YjfEAScnURx53VIMoh+VR
# 0t8ewQ/n8ALYlmLxoYIPFjCCDxIGCisGAQQBgjcDAwExgg8CMIIO/gYJKoZIhvcN
# AQcCoIIO7zCCDusCAQMxDTALBglghkgBZQMEAgEwdwYLKoZIhvcNAQkQAQSgaARm
# MGQCAQEGDCsGAQQBgqkwAQMGATAxMA0GCWCGSAFlAwQCAQUABCBPBleJWJ0WbR3X
# ewjRdnzPbroSz99+zqpDDU6r2BU9LQIII0LTyBuF2MgYDzIwMjUwMjE2MjAxMDQ5
# WjADAgEBoIIMADCCBPwwggLkoAMCAQICEFparOgaNW60YoaNV33gPccwDQYJKoZI
# hvcNAQELBQAwczELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQH
# DAdIb3VzdG9uMREwDwYDVQQKDAhTU0wgQ29ycDEvMC0GA1UEAwwmU1NMLmNvbSBU
# aW1lc3RhbXBpbmcgSXNzdWluZyBSU0EgQ0EgUjEwHhcNMjQwMjE5MTYxODE5WhcN
# MzQwMjE2MTYxODE4WjBuMQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAO
# BgNVBAcMB0hvdXN0b24xETAPBgNVBAoMCFNTTCBDb3JwMSowKAYDVQQDDCFTU0wu
# Y29tIFRpbWVzdGFtcGluZyBVbml0IDIwMjQgRTEwWTATBgcqhkjOPQIBBggqhkjO
# PQMBBwNCAASnYXL1MOl6xIMUlgVC49zonduUbdkyb0piy2i8t3JlQEwA74cjK8g9
# mRC8GH1cAAVMIr8M2HdZpVgkV1LXBLB8o4IBWjCCAVYwHwYDVR0jBBgwFoAUDJ0Q
# JY6apxuZh0PPCH7hvYGQ9M8wUQYIKwYBBQUHAQEERTBDMEEGCCsGAQUFBzAChjVo
# dHRwOi8vY2VydC5zc2wuY29tL1NTTC5jb20tdGltZVN0YW1waW5nLUktUlNBLVIx
# LmNlcjBRBgNVHSAESjBIMDwGDCsGAQQBgqkwAQMGATAsMCoGCCsGAQUFBwIBFh5o
# dHRwczovL3d3dy5zc2wuY29tL3JlcG9zaXRvcnkwCAYGZ4EMAQQCMBYGA1UdJQEB
# /wQMMAoGCCsGAQUFBwMIMEYGA1UdHwQ/MD0wO6A5oDeGNWh0dHA6Ly9jcmxzLnNz
# bC5jb20vU1NMLmNvbS10aW1lU3RhbXBpbmctSS1SU0EtUjEuY3JsMB0GA1UdDgQW
# BBRQTySs77U+YxMjCZIm7Lo6luRdIjAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcN
# AQELBQADggIBAJigjwMAkbyrxGRBf0Ih4r+rbCB57lTuwViC6nH2fZSciMogpqSz
# rSeVZ2eIb5vhj9rT7jqWXZn02Fncs4YTrA1QyxJW36yjC4jl5/bsFCaWuXzGXt2Y
# 6Ifp//A3Z0sNTMWTTBobmceM3sqnovdX9ToRFP+29r5yQnPcgRTI2PvrVSqLxY9E
# yk9/0cviM3W29YBl080ENblRcu3Y8RsfzRtVT/2snuDocRxvRYmd0TPaMgIj2xII
# 651QnPp1hiq9xU0AyovLzbsi5wlR5Ip4i/i8+x+HwYJNety5cYtdWJ7uQP6YaZtW
# /jNoHp76qNftq/IlSx6xEYBRjFBxHSq2fzhUQ5oBawk2OsZ2j0wOf7q7AqjCt6t/
# +fbmWjrAWYWZGj/RLjltqdFPBpIKqdhjVIxaGgzVhaE/xHKBg4k4DfFZkBYJ9BWu
# P93Tm+paWBDwXI7Fg3alGsboErWPWlvwMAmpeJUjeKLZY26JPLt9ZWceTVWuIyuj
# erqb5IMmeqLJm5iFq/Qy4YPGyPiolw5w1k9OeO4ErmS2FKvk1ejvw4SWR+S1VyWn
# ktY442WaoStxBCCVWZdMWFeB+EpL8uoQNq1MhSt/sIUjUudkyZLIbMVQjj7b6gPX
# nD6mS8FgWiCAhuM1a/hgA+6o1sJWizHdmcpYDhyNzorf9KVRE6iR7rcmMIIG/DCC
# BOSgAwIBAgIQbVIYcIfoI02FYADQgI+TVjANBgkqhkiG9w0BAQsFADB8MQswCQYD
# VQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0b24xGDAWBgNV
# BAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBSb290IENlcnRp
# ZmljYXRpb24gQXV0aG9yaXR5IFJTQTAeFw0xOTExMTMxODUwMDVaFw0zNDExMTIx
# ODUwMDVaMHMxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwH
# SG91c3RvbjERMA8GA1UECgwIU1NMIENvcnAxLzAtBgNVBAMMJlNTTC5jb20gVGlt
# ZXN0YW1waW5nIElzc3VpbmcgUlNBIENBIFIxMIICIjANBgkqhkiG9w0BAQEFAAOC
# Ag8AMIICCgKCAgEArlEQE9L5PCCgIIXeyVAcZMnh/cXpNP8KfzFI6HJaxV6oYf3x
# h/dRXPu35tDBwhOwPsJjoqgY/Tg6yQGBqt65t94wpx0rAgTVgEGMqGri6vCI6rEt
# SZVy9vagzTDHcGfFDc0Eu71mTAyeNCUhjaYTBkyANqp9m6IRrYEXOKdd/eREsqVD
# mhryd7dBTS9wbipm+mHLTHEFBdrKqKDM3fPYdBOro3bwQ6OmcDZ1qMY+2Jn1o0l4
# N9wORrmPcpuEGTOThFYKPHm8/wfoMocgizTYYeDG/+MbwkwjFZjWKwb4hoHT2WK8
# pvGW/OE0Apkrl9CZSy2ulitWjuqpcCEm2/W1RofOunpCm5Qv10T9tIALtQo73GHI
# lIDU6xhYPH/ACYEDzgnNfwgnWiUmMISaUnYXijp0IBEoDZmGT4RTguiCmjAFF5OV
# NbY03BQoBb7wK17SuGswFlDjtWN33ZXSAS+i45My1AmCTZBV6obAVXDzLgdJ1A1r
# yyXz4prLYyfJReEuhAsVp5VouzhJVcE57dRrUanmPcnb7xi57VPhXnCuw26hw1Hd
# +ulK3jJEgbc3rwHPWqqGT541TI7xaldaWDo85k4lR2bQHPNGwHxXuSy3yczyOg57
# TcqqG6cE3r0KR6jwzfaqjTvN695GsPAPY/h2YksNgF+XBnUD9JBtL4c34AcCAwEA
# AaOCAYEwggF9MBIGA1UdEwEB/wQIMAYBAf8CAQAwHwYDVR0jBBgwFoAU3QQJB6L1
# en1SUxKSle44gCUNplkwgYMGCCsGAQUFBwEBBHcwdTBRBggrBgEFBQcwAoZFaHR0
# cDovL3d3dy5zc2wuY29tL3JlcG9zaXRvcnkvU1NMY29tUm9vdENlcnRpZmljYXRp
# b25BdXRob3JpdHlSU0EuY3J0MCAGCCsGAQUFBzABhhRodHRwOi8vb2NzcHMuc3Ns
# LmNvbTA/BgNVHSAEODA2MDQGBFUdIAAwLDAqBggrBgEFBQcCARYeaHR0cHM6Ly93
# d3cuc3NsLmNvbS9yZXBvc2l0b3J5MBMGA1UdJQQMMAoGCCsGAQUFBwMIMDsGA1Ud
# HwQ0MDIwMKAuoCyGKmh0dHA6Ly9jcmxzLnNzbC5jb20vc3NsLmNvbS1yc2EtUm9v
# dENBLmNybDAdBgNVHQ4EFgQUDJ0QJY6apxuZh0PPCH7hvYGQ9M8wDgYDVR0PAQH/
# BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQCSGXUNplpCzxkH2fL8lPrAm/AV6USW
# Wi9xM91Q5RN7mZN3D8T7cm1Xy7qmnItFukgdtiUzLbQokDJyFTrF1pyLgGw/2hU3
# FJEywSN8crPsBGo812lyWFgAg0uOwUYw7WJQ1teICycX/Fug0KB94xwxhsvJBiRT
# pQyhu/2Kyu1Bnx7QQBA1XupcmfhbQrK5O3Q/yIi//kN0OkhQEiS0NlyPPYoRboHW
# C++wogzV6yNjBbKUBrMFxABqR7mkA0x1Kfy3Ud08qyLC5Z86C7JFBrMBfyhfPpKV
# lIiiTQuKz1rTa8ZW12ERoHRHcfEjI1EwwpZXXK5J5RcW6h7FZq/cZE9kLRZhvnRK
# tb+X7CCtLx2h61ozDJmifYvuKhiUg9LLWH0Or9D3XU+xKRsRnfOuwHWuhWch8G7k
# EmnTG9CtD9Dgtq+68KgVHtAWjKk2ui1s1iLYAYxnDm13jMZm0KpRM9mLQHBK5Gb4
# dFgAQwxOFPBslf99hXWgLyYE33vTIi9p0gYqGHv4OZh1ElgGsvyKdUUJkAr5hfbD
# X6pYScJI8v9VNYm1JEyFAV9x4MpskL6kE2Sy8rOqS9rQnVnIyPWLi8N9K4GZvPit
# /Oy+8nFL6q5kN2SZbox5d69YYFe+rN1sDD4CpNWwBBTI/q0V4pkgvhL99IV2Xasj
# HZf4peSrHdL4RjGCAlgwggJUAgEBMIGHMHMxCzAJBgNVBAYTAlVTMQ4wDAYDVQQI
# DAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjERMA8GA1UECgwIU1NMIENvcnAxLzAt
# BgNVBAMMJlNTTC5jb20gVGltZXN0YW1waW5nIElzc3VpbmcgUlNBIENBIFIxAhBa
# WqzoGjVutGKGjVd94D3HMAsGCWCGSAFlAwQCAaCCAWEwGgYJKoZIhvcNAQkDMQ0G
# CyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNTAyMTYyMDEwNDlaMCgGCSqG
# SIb3DQEJNDEbMBkwCwYJYIZIAWUDBAIBoQoGCCqGSM49BAMCMC8GCSqGSIb3DQEJ
# BDEiBCDE0zhFRjUv3mYrNzNlCw7l/HvKFE57wlgXeQu3ZRiJsjCByQYLKoZIhvcN
# AQkQAi8xgbkwgbYwgbMwgbAEIJ1xf43CN2Wqzl5KsOH1ddeaF9Qc7tj9r+8D/T29
# iUfnMIGLMHekdTBzMQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNV
# BAcMB0hvdXN0b24xETAPBgNVBAoMCFNTTCBDb3JwMS8wLQYDVQQDDCZTU0wuY29t
# IFRpbWVzdGFtcGluZyBJc3N1aW5nIFJTQSBDQSBSMQIQWlqs6Bo1brRiho1XfeA9
# xzAKBggqhkjOPQQDAgRHMEUCIFKQjJb8aspRAiB6PlfpBzW5wXyRP0wDTns4tR1n
# Xn1eAiEA//5C2IRovXpKU08AAxKz+CNI2bUNwuQ4LAq67YF/b0U=
# SIG # End signature block