AzStackHciExternalActiveDirectory/AzStackHci.ExternalActiveDirectory.Tests.psm1

Import-LocalizedData -BindingVariable lcAdTxt -FileName AzStackHci.ExternalActiveDirectory.Strings.psd1

class HealthModel
{
    # Attributes for Azure Monitor schema
    [string]$Name #Name of the individual test/rule/alert that was executed. Unique, not exposed to the customer.
    [string]$Title #User-facing name; one or more sentences indicating the direct issue.
    [string]$Severity #Severity of the result (Critical, Warning, Informational, Hidden) – this answers how important the result is. Critical is the only update-blocking severity.
    [string]$Description #Detailed overview of the issue and what impact the issue has on the stamp.
    [psobject]$Tags #Key-value pairs that allow grouping/filtering individual tests. For example, "Group": "ReadinessChecks", "UpdateType": "ClusterAware"
    [string]$Status #The status of the check running (i.e. Failed, Succeeded, In Progress) – this answers whether the check ran, and passed or failed.
    [string]$Remediation #Set of steps that can be taken to resolve the issue found.
    [string]$TargetResourceID #The unique identifier for the affected resource (such as a node or drive).
    [string]$TargetResourceName #The name of the affected resource.
    [string]$TargetResourceType #The type of resource being referred to (well-known set of nouns in infrastructure, aligning with Monitoring).
    [datetime]$Timestamp #The Time in which the HealthCheck was called.
    [psobject[]]$AdditionalData #Property bag of key value pairs for additional information.
    [string]$HealthCheckSource #The name of the services called for the HealthCheck (I.E. Test-AzureStack, Test-Cluster).
}

class OrganizationalUnitTestResult : HealthModel {}

class ExternalADTest
{
    [string]$TestName
    [scriptblock]$ExecutionBlock
}

$ExternalAdTestInitializors = @(
)

$ExternalAdTests = @(
    <# Can't execute this test during deployment as Get-KdsRootKey will try to access the DVM KDS and come up with an empty value
    (New-Object -Type ExternalADTest -Property @{
        TestName = "KdsRootKeyExists"
        ExecutionBlock = {
            Param ([hashtable]$testContext)
 
            $dcName = $null
            $KdsRootKey = $null
            $accessDenied = $false
            try
            {
                # Must use the server name and credentials that are passed in if they exist
                $getDomainControllerParams = @{}
                if ($testContext["AdServer"] -and $testContext["AdCredentials"])
                {
                    $getDomainControllerParams += @{Server = $testContext["AdServer"]}
                    $getDomainControllerParams += @{Credential = $testContext["AdCredentials"]}
                }
                else
                {
                    $getDomainControllerParams += @{DomainName = $testContext["DomainFQDN"]}
                    $getDomainControllerParams += @{MinimumDirectoryServiceVersion = "Windows2012"}
                    $getDomainControllerParams += @{NextClosestSite = $true}
                    $getDomainControllerParams += @{Discover = $true}
                }
                $adDomainController = Get-ADDomainController @getDomainControllerParams
                $dcName = "$($adDomainController.Name).$($adDomainController.Domain)"
 
                # This cmdlet doesn't take a server name or credentials, so it may fail when not run from a domain-joined machine
                $KdsRootKey = $null
                try
                {
                    $KdsRootKey = Get-KdsRootKey
                }
                catch
                {
                    $accessDenied = $true
                }
 
                if($KdsRootKey)
                {
                    # make sure it is effective at least 10 hours ago
                    if(((Get-Date) - $KdsRootKey.EffectiveTime).TotalHours -lt 10)
                    {
                        $KdsRootKey = $null
                    }
                }
            }
            catch {}
 
            $rootKeyStatus = if ($dcName -and $KdsRootKey) { 'Succeeded' } else { 'Failed' }
            if ($accessDenied)
            {
                $rootKeyStatus = 'Skipped'
            }
            return New-Object PSObject -Property @{
                Resource = "KdsRootKey"
                Status = $rootKeyStatus
                TimeStamp = [datetime]::UtcNow
                Source = $ENV:COMPUTERNAME
                Detail = $testContext["LcAdTxt"].KdsRootKeyMissingRemediation
            }
        }
    }),
    #>

    (New-Object -Type ExternalADTest -Property @{
        TestName = "RequiredOrgUnitsExist"
        ExecutionBlock = {
            Param ([hashtable]$testContext)

            $serverParams = @{}
            if ($testContext["AdServer"])
            {
                $serverParams += @{Server = $testContext["AdServer"]}
            }
            if ($testContext["AdCredentials"])
            {
                $serverParams += @{Credential = $testContext["AdCredentials"]}
            }

            $requiredOUs = @($testContext["ADOUPath"], $testContext["ComputersADOUPath"], $testContext["UsersADOUPath"])

            Log-Info -Message (" Checking for the existance of OUs: {0}" -f ($requiredOUs -join ", ")) -Type Info -Function "RequiredOrgUnitsExist"

            $results = $requiredOUs | ForEach-Object {
                $resultingOU = $null

                try {
                    $resultingOU = Get-ADOrganizationalUnit -Identity $_ -ErrorAction SilentlyContinue @serverParams
                }
                catch {
                }

                return New-Object PSObject -Property @{
                    Resource    = $_
                    Status      = if ($resultingOU) { 'Succeeded' } else { 'Failed' }
                    TimeStamp   = [datetime]::UtcNow
                    Source      = $ENV:COMPUTERNAME
                    Detail = ($testContext["LcAdTxt"].MissingOURemediation -f $_)
                }
            }

            return $results
        }
    }),
    (New-Object -Type ExternalADTest -Property @{
        TestName = "PhysicalMachineObjectsExist"
        ExecutionBlock = {
            Param ([hashtable]$testContext)

            $serverParams = @{}
            if ($testContext["AdServer"])
            {
                $serverParams += @{Server = $testContext["AdServer"]}
            }
            if ($testContext["AdCredentials"])
            {
                $serverParams += @{Credential = $testContext["AdCredentials"]}
            }

            $computerAdOuPath = $testContext["ComputersADOUPath"]
            $domainFQDN = $testContext["DomainFQDN"]

            $physicalHostsSetting = @($testContext["PhysicalMachineNames"] | Where-Object { -not [string]::IsNullOrEmpty($_) })

            Log-Info -Message (" Validating settings for physical hosts: {0}" -f ($physicalHostsSetting -join ", ")) -Type Info -Function "PhysicalMachineObjectsExist"

            try {
                $allComputerObjects = Get-ADComputer -Filter "*" @serverParams
            }
            catch {
                Log-Info -Message (" Failed to find any computer objects in ActiveDirectory. Inner exception: {0}" -f $_) -Type Error -Function "PhysicalMachineObjectsExist"
                $allComputerObjects = @()
            }

            $foundPhysicalHosts = @($allComputerObjects | Where-Object {$_.Name -in $physicalHostsSetting})
            $missingPhysicalHostEntries = @($physicalHostsSetting | Where-Object {$_ -notin $allComputerObjects.Name})

            Log-Info -Message (" Found {0} entries in AD : {1}" -f $foundPhysicalHosts.Count,($foundPhysicalHosts.Name -join ", ")) -Type Info -Function "PhysicalMachineObjectsExist"

            $physicalHostsWithBadDnsName = $($foundPhysicalHosts | Where-Object { $_.DNSHostName -ne "$($_.Name).$domainFQDN" })
            $physicalHostsWithBadSAMAcct = $($foundPhysicalHosts | Where-Object { $_.SAMAccountName -ne "$($_.Name)$" })

            Log-Info -Message (" Found {0} entries with invalid DNS names: {1}" -f $physicalHostsWithBadDnsName.Count,(($physicalHostsWithBadDnsName | Select-Object -Property DNSHostName) -join ", ")) -Type Info -Function "PhysicalMachineObjectsExist"
            Log-Info -Message (" Found {0} entries with invalid SAM account names: {1}" -f $physicalHostsWithBadSAMAcct.Count,(($physicalHostsWithBadSAMAcct | Select-Object -Property SAMAccountName) -join ", ")) -Type Info -Function "PhysicalMachineObjectsExist"

            $results = @()

            $hasComputerEntries = ($foundPhysicalHosts -and $foundPhysicalHosts.Count -eq $physicalHostsSetting.Count)
            $allEntriesHaveCorrectDnsNames = (-not $physicalHostsWithBadDnsName -or $physicalHostsWithBadDnsName.Count -eq 0)
            $allEntriesHaveCorrectSamAcct = (-not $physicalHostsWithBadSAMAcct -or $physicalHostsWithBadSAMAcct.Count -eq 0)

            $results += New-Object PSObject -Property @{
                Resource    = "PhysicalHostAdComputerEntries"
                Status      = if ($hasComputerEntries) { 'Succeeded' } else { 'Failed' }
                TimeStamp   = [datetime]::UtcNow
                Source      = $ENV:COMPUTERNAME
                Detail = ($testContext["LcAdTxt"].HostsMissingRemediation -f ($missingPhysicalHostEntries -join ", "),$computerAdOuPath)
            }
            $results += New-Object PSObject -Property @{
                Resource    = "PhysicalHostAdComputerDnsNames"
                Status      = if ($allEntriesHaveCorrectDnsNames) { 'Succeeded' } else { 'Failed' }
                TimeStamp   = [datetime]::UtcNow
                Source      = $ENV:COMPUTERNAME
                Detail = ($testContext["LcAdTxt"].HostsWithIncorrectDnsNameRemediation -f ($physicalHostsWithBadDnsName.Name -join ", "))
            }
            $results += New-Object PSObject -Property @{
                Resource    = "PhysicalHostAdComputerSamAccounts"
                Status      = if ($allEntriesHaveCorrectSamAcct) { 'Succeeded' } else { 'Failed' }
                TimeStamp   = [datetime]::UtcNow
                Source      = $ENV:COMPUTERNAME
                Detail = ($testContext["LcAdTxt"].HostsWithIncorrectSamAcctRemediation -f ($physicalHostsWithBadDnsName.Name -join ", "))
            }

            return $results
        }
    }),
    (New-Object -Type ExternalADTest -Property @{
        TestName = "ClusterExistsWithRequiredAcl"
        ExecutionBlock = {
            Param ([hashtable]$testContext)

            $serverParams = @{}
            if ($TestContext["AdServer"])
            {
                $serverParams += @{Server = $TestContext["AdServer"]}
            }
            if ($TestContext["AdCredentials"])
            {
                $serverParams += @{Credential = $TestContext["AdCredentials"]}
            }

            $createChildAce = $null
            $readPropertyAce = $null
            $computersAdOuPath = $testContext["ComputersADOUPath"]
            $clusterName = $testContext["ClusterName"]

            Log-Info -Message (" Searching for '{0}' entry in OU '{1}'" -f $clusterName,$computersAdOuPath) -Type Info -Function "ClusterExistsWithRequiredAcl"

            try {
                $clusterComputerEntry = Get-ADComputer -SearchBase $computersAdOuPath -Filter "Name -eq '$clusterName'" @serverParams
            }
            catch {
                Log-Info -Message (" Failed to find '{0}' entry in OU '{1}'. Inner exception: {2}" -f $clusterName,$computersAdOuPath,$_) -Type Error -Function "ClusterExistsWithRequiredAcl"
                $clusterComputerEntry = $null
            }

            if ($clusterComputerEntry)
            {
                $clusterSID = $clusterComputerEntry.SID
                Log-Info -Message (" Found entry, SID: {0}" -f $clusterSID) -Type Info -Function "ClusterExistsWithRequiredAcl"

                # The AD module SHOULD install a drive that we can use to get ACLs. However, sometimes it isn't properly registered
                # especially if we just installed it. So verify that it's usable
                $adDriveName = "AD"
                $tempDriveName = "hciad"
                $adDriveObject = $null

                try
                {
                    $adProvider = Get-PSProvider -PSProvider ActiveDirectory
                    if ($adProvider -and $adProvider.Drives.Count -gt 0)
                    {
                        $adDriveObject = $adProvider.Drives | Where-Object {$_.Name -eq $adDriveName -or $_.Name -eq $tempDriveName}
                    }
                }
                catch {
                    Log-Info -Message (" Error while trying to access active directory PS drive. Will fall back to creating a new PS drive. Inner exception: {0}" -f $_) -Type Warning -Function "ClusterExistsWithRequiredAcl"
                }

                if (-not $adDriveObject)
                {
                    try {
                        # Add a new drive
                        $adDriveObject = New-PSDrive -Name $tempDriveName -PSProvider ActiveDirectory -Root '' @serverParams
                    }
                    catch {
                        Log-Info -Message (" Error while trying to create active directory PS drive. Inner exception: {0}" -f $_) -Type Error -Function "ClusterExistsWithRequiredAcl"
                    }
                }

                $accessRules = @()

                if ($adDriveObject)
                {
                    $adDriveName = $adDriveObject.Name

                    try
                    {
                        $ouPath = ("{0}:\{1}" -f $adDriveName,$computersAdOuPath)
                        $ouAcl = Get-Acl $ouPath
                    }
                    catch
                    {
                        Log-Info -Message (" Can't get acls from {0}. Inner exception: {1}" -f $ouPath,$_) -Type Error -Function "ClusterExistsWithRequiredAcl"
                    }
                    finally {
                        # best effort cleanup if we had added the temp drive
                        try
                        {
                            if ($adDriveName -eq $tempDriveName)
                            {
                                $adDriveObject | Remove-PSDrive
                            }
                        }
                        catch {}
                    }

                    try {
                        # must specify the type to retrieve -- need to get something comparable to the clusterSID
                        if ($ouAcl)
                        {
                            $accessRules = $ouAcl.GetAccessRules($true, $true, $clusterSID.GetType())
                        }
                    }
                    catch {
                        Log-Info -Message (" Error while trying to get access rules for OU. Inner exception: {0}" -f $_) -Type Error -Function "ClusterExistsWithRequiredAcl"
                    }
                }

                # Check that the CreateChild ACE has been added
                $createChildAce = $accessRules | Where-Object { `
                    $_.IdentityReference -eq $clusterSID -and `
                    $_.ActiveDirectoryRights -bor [System.DirectoryServices.ActiveDirectoryRights]::CreateChild -and `
                    $_.AccessControlType -eq [System.Security.AccessControl.AccessControlType]::Allow -and `
                    $_.ObjectType -eq ([System.Guid]::New('bf967a86-0de6-11d0-a285-00aa003049e2')) -and
                    $_.InheritanceType -eq [System.DirectoryServices.ActiveDirectorySecurityInheritance]::All
                }
                $readPropertyAce = $accessRules | Where-Object { `
                    $_.IdentityReference -eq $clusterSID -and `
                    $_.ActiveDirectoryRights -bor [System.DirectoryServices.ActiveDirectoryRights]::ReadProperty -and `
                    $_.AccessControlType -eq [System.Security.AccessControl.AccessControlType]::Allow -and `
                    $_.ObjectType -eq [System.Guid]::Empty -and
                    $_.InheritanceType -eq [System.DirectoryServices.ActiveDirectorySecurityInheritance]::All
                }

                Log-Info -Message (" Found CreateChild ACE: {0}" -f ([bool]$createChildAce)) -Type Info -Function "ClusterExistsWithRequiredAcl"
                Log-Info -Message (" Found ReadProperty ACE: {0}" -f ([bool]$readPropertyAce)) -Type Info -Function "ClusterExistsWithRequiredAcl"
            }

            return New-Object PSObject -Property @{
                Resource    = "ClusterAcls"
                Status      = if ($createChildAce -and $readPropertyAce) { 'Succeeded' } else { 'Failed' }
                TimeStamp   = [datetime]::UtcNow
                Source      = $ENV:COMPUTERNAME
                Detail = ($testContext["LcAdTxt"].ClusterAclsMissingRemediation -f $computersAdOuPath)
            }
        }
    }),
    (New-Object -Type ExternalADTest -Property @{
        TestName = "SecurityGroupsExist"
        ExecutionBlock = {
            Param ([hashtable]$testContext)

            $serverParams = @{}
            if ($TestContext["AdServer"])
            {
                $serverParams += @{Server = $TestContext["AdServer"]}
            }
            if ($TestContext["AdCredentials"])
            {
                $serverParams += @{Credential = $TestContext["AdCredentials"]}
            }

            $securityGroups = @(
                "$($testContext["NamingPrefix"])-Sto-SG",
                "$($testContext["NamingPrefix"])-FsAcl-InfraSG",
                "$($testContext["NamingPrefix"])-EceSG",
                "$($testContext["NamingPrefix"])-BM-ECESG",
                "$($testContext["NamingPrefix"])-HA-R-SrvSG",
                "$($testContext["NamingPrefix"])-Hc-Rs-SrvSG",
                "$($testContext["NamingPrefix"])-IH-HsSG",
                "$($testContext["NamingPrefix"])-OpsAdmin",
                "$($testContext["NamingPrefix"])-SB-Jea-MG-VmSG",
                "$($testContext["NamingPrefix"])-IH-MsSG",
                "$($testContext["NamingPrefix"])-DLSG"
            )

            $usersOuPath = $testContext["UsersADOUPath"]

            $missingSecurityGroups = @()

            # Look up all the required security groups and identify any that are missing
            foreach ($securityGroup in $securityGroups)
            {
                try {
                    $adGroup = Get-AdGroup -SearchBase $usersOuPath -Filter { Name -eq $securityGroup } @serverParams
                }
                catch {
                    $adGroup = $null
                }

                if (-not $adGroup)
                {
                    $missingSecurityGroups += $securityGroup
                }
            }

            $physicalHosts = @()

            # get the list of physical hosts
            foreach ($host in $testContext["PhysicalMachineNames"])
            {
                $physicalHosts += @{Name=$host; Object=(Get-ADComputer -Identity $host @serverParams); MissingSGs=@()}
            }

            # Now check that the physical machines have been added to the required SGs
            $physicalMachineSecurityGroups = @("$($testContext["NamingPrefix"])-Sto-SG")

            foreach ($physicalHost in $physicalHosts)
            {
                foreach ($physicalMachineSecurityGroup in $physicalMachineSecurityGroups)
                {
                    $isMember = $false
                    try
                    {
                        $groupObject = Get-ADGroup -SearchBase $usersOuPath -Filter {Name -eq $physicalMachineSecurityGroup} @serverParams

                        if ($groupObject)
                        {
                            $isMember = Get-ADGroupMember -Identity $groupObject @serverParams | Where-Object {$_.SID -eq $($physicalHost.Object.SID)}
                        }
                    }
                    catch
                    {
                        Log-Info -Message (" Failed to check whether host '{0}' is a member of group '{1}'. Inner exception: {2}" -f $physicalHost.Name,$physicalMachineSecurityGroup,$_) -Type Warning -Function "ClusterExistsWithRequiredAcl"
                    }

                    if (-not $isMember)
                    {
                        $physicalHost.MissingSGs += $physicalMachineSecurityGroup
                    }
                }
            }

            $results = @()

            $results += New-Object PSObject -Property @{
                Resource    = "SecurityGroups"
                Status      = if ($missingSecurityGroups.Count -eq 0) { 'Succeeded' } else { 'Failed' }
                TimeStamp   = [datetime]::UtcNow
                Source      = $ENV:COMPUTERNAME
                Detail = ($testContext["LcAdTxt"].SecurityGroupsMissingRemediation -f ($missingSecurityGroups -join ', '))
            }

            foreach ($physicalHost in $physicalHosts)
            {
                $missingSecurityGroupMemberships = $physicalHost.MissingSGs

                $results += New-Object PSObject -Property @{
                    Resource    = "SecurityGroupMembership_$($physicalHost.Name)"
                    Status      = if ($missingSecurityGroupMemberships.Count -eq 0) { 'Succeeded' } else { 'Failed' }
                    TimeStamp   = [datetime]::UtcNow
                    Source      = $ENV:COMPUTERNAME
                    Detail = ($testContext["LcAdTxt"].HostSecurityGroupsMissingRemediation -f $physicalHost.Name,($missingSecurityGroupMemberships -join ', '))
                }
            }

            return $results
        }
    }),
    (New-Object -Type ExternalADTest -Property @{
        TestName = "gMSAsExist"
        ExecutionBlock = {
            Param ([hashtable]$testContext)

            $usersOuPath = $testContext["UsersADOUPath"]

            $serverParams = @{}
            if ($TestContext["AdServer"])
            {
                $serverParams += @{Server = $TestContext["AdServer"]}
            }
            if ($TestContext["AdCredentials"])
            {
                $serverParams += @{Credential = $TestContext["AdCredentials"]}
            }

            $gmsaAccounts = @(
                [pscustomobject]@{ GmsaName="$($testContext["NamingPrefix"])-BM-ECE";
                                     PrincipalsAllowedToRetrieveManagedPassword= @("$($testContext["NamingPrefix"])-Sto-SG");
                                     ServicePrincipalName=@("$($testContext["NamingPrefix"])/ae3299a9-3e87-4186-bd99-c43c9ae6a571");
                                     MemberOf=@("$($testContext["NamingPrefix"])-EceSG", "$($testContext["NamingPrefix"])-BM-ECESG", "$($testContext["NamingPrefix"])-FsAcl-InfraSG")},

                [pscustomobject]@{ GmsaName="$($testContext["NamingPrefix"])-BM-ALM";
                                     PrincipalsAllowedToRetrieveManagedPassword= @("$($testContext["NamingPrefix"])-Sto-SG");
                                     ServicePrincipalName=@();
                                     MemberOf=@("$($testContext["NamingPrefix"])-FsAcl-InfraSG")},

                [pscustomobject]@{ GmsaName="$($testContext["NamingPrefix"])-BM-FCA";
                                     PrincipalsAllowedToRetrieveManagedPassword= @("$($testContext["NamingPrefix"])-Sto-SG");
                                     ServicePrincipalName=@();
                                     MemberOf=@("$($testContext["NamingPrefix"])-FsAcl-InfraSG")},

                [pscustomobject]@{ GmsaName="$($testContext["NamingPrefix"])-BM-FRA";
                                     PrincipalsAllowedToRetrieveManagedPassword= @("$($testContext["NamingPrefix"])-Sto-SG");
                                     ServicePrincipalName=@();
                                     MemberOf=@("$($testContext["NamingPrefix"])-FsAcl-InfraSG")},

                [pscustomobject]@{ GmsaName="$($testContext["NamingPrefix"])-BM-TCA";
                                     PrincipalsAllowedToRetrieveManagedPassword= @("$($testContext["NamingPrefix"])-Sto-SG");
                                     ServicePrincipalName=@();
                                     MemberOf=@("$($testContext["NamingPrefix"])-FsAcl-InfraSG")},

                [pscustomobject]@{ GmsaName="$($testContext["NamingPrefix"])-BM-HA";
                                     PrincipalsAllowedToRetrieveManagedPassword= @("$($testContext["NamingPrefix"])-Sto-SG");
                                     ServicePrincipalName=@("$($testContext["NamingPrefix"])/PhysicalNode/1b4dde6b-7ea8-407a-8c9e-f86e8b97fd1c");
                                     MemberOf=@("$($testContext["NamingPrefix"])-FsAcl-InfraSG", "$($testContext["NamingPrefix"])-HA-R-SrvSG")},

                [pscustomobject]@{ GmsaName="$($testContext["NamingPrefix"])-SB-LC";
                                     PrincipalsAllowedToRetrieveManagedPassword= @("$($testContext["NamingPrefix"])-Sto-SG");
                                     ServicePrincipalName=@("$($testContext["NamingPrefix"])/754dbc04-8f91-4cb6-a10f-899dac573fa0");
                                     MemberOf=@("$($testContext["NamingPrefix"])-FsAcl-InfraSG", "$($testContext["NamingPrefix"])-Sto-SG" ) },

                [pscustomobject]@{ GmsaName="$($testContext["NamingPrefix"])-SB-Jea";
                                     PrincipalsAllowedToRetrieveManagedPassword= @("$($testContext["NamingPrefix"])-Sto-SG");
                                     ServicePrincipalName=@();
                                     MemberOf=@("$($testContext["NamingPrefix"])-FsAcl-InfraSG", "$($testContext["NamingPrefix"])-Sto-SG" ) },

                [pscustomobject]@{ GmsaName="$($testContext["NamingPrefix"])-SB-MG";
                                     PrincipalsAllowedToRetrieveManagedPassword= @("$($testContext["NamingPrefix"])-Sto-SG", "$($testContext["NamingPrefix"])-SB-Jea-MG-VmSG");
                                     ServicePrincipalName=@("$($testContext["NamingPrefix"])/ea126685-c89e-4294-959f-bba6bf75b4aa");
                                     MemberOf=@("$($testContext["NamingPrefix"])-FsAcl-InfraSG", "$($testContext["NamingPrefix"])-SB-Jea-MG-VmSG", "$($testContext["NamingPrefix"])-Sto-SG" ) },

                [pscustomobject]@{ GmsaName="$($testContext["NamingPrefix"])-SBJeaM";
                                     PrincipalsAllowedToRetrieveManagedPassword= @("$($testContext["NamingPrefix"])-Sto-SG");
                                     ServicePrincipalName=@();
                                     MemberOf=@("$($testContext["NamingPrefix"])-FsAcl-InfraSG", "$($testContext["NamingPrefix"])-Sto-SG" ) },

                [pscustomobject]@{ GmsaName="$($testContext["NamingPrefix"])-EceSA";
                                     PrincipalsAllowedToRetrieveManagedPassword= @("$($testContext["NamingPrefix"])-Sto-SG");
                                     ServicePrincipalName=@("$($testContext["NamingPrefix"])/4dde37cc-6ee0-4d75-9444-7061e156507f");
                                     MemberOf=@("$($testContext["NamingPrefix"])-FsAcl-InfraSG", "$($testContext["NamingPrefix"])-EceSG", "$($testContext["NamingPrefix"])-BM-EceSG")},

                [pscustomobject]@{ GmsaName="$($testContext["NamingPrefix"])-Urp-SA";
                                     PrincipalsAllowedToRetrieveManagedPassword= @("$($testContext["NamingPrefix"])-Sto-SG");
                                     ServicePrincipalName=@("$($testContext["NamingPrefix"])/110bac92-1879-47ae-9611-e40f8abf4fc0");
                                     MemberOf=@("$($testContext["NamingPrefix"])-FsAcl-InfraSG", "$($testContext["NamingPrefix"])-BM-ECESG", "$($testContext["NamingPrefix"])-EceSG", "$($testContext["NamingPrefix"])-DLSG")},

                [pscustomobject]@{ GmsaName="$($testContext["NamingPrefix"])-DL-SA";
                                     PrincipalsAllowedToRetrieveManagedPassword= @("$($testContext["NamingPrefix"])-Sto-SG");
                                     ServicePrincipalName=@("$($testContext["NamingPrefix"])/365645b4-f9a5-4a7d-8669-c08a1c41d66b");
                                     MemberOf=@("$($testContext["NamingPrefix"])-FsAcl-InfraSG", "$($testContext["NamingPrefix"])-BM-ECESG", "$($testContext["NamingPrefix"])-EceSG", "$($testContext["NamingPrefix"])-DLSG")},

                [pscustomobject]@{ GmsaName="$($testContext["NamingPrefix"])-BM-MSA";
                                     PrincipalsAllowedToRetrieveManagedPassword= @("$($testContext["NamingPrefix"])-Sto-SG");
                                     ServicePrincipalName=@("$($testContext["NamingPrefix"])/PhysicalNode/d8c180f6-7290-458e-90f0-96894f45e981");
                                     MemberOf=@("$($testContext["NamingPrefix"])-FsAcl-InfraSG",  "$($testContext["NamingPrefix"])-IH-MsSG", "$($testContext["NamingPrefix"])-HA-R-SrvSG")}
            )

            $missingGmsaAccounts = @()

            foreach ($gmsaAccount in $gmsaAccounts)
            {
                $accountMissing = $true
                try
                {
                    $gmsaName = $gmsaAccount.GmsaName
                    $adGmsaAccount = Get-ADServiceAccount -SearchBase $usersOuPath -Filter {Name -eq $gmsaName} @serverParams
                    if ($adGmsaAccount)
                    {
                        # TODO, identify SPNs and make sure they match

                        # TODO, identify PrincipasAllowedToRetrieveManagedPassword and check

                        $isMember = $true
                        try
                        {
                            foreach ($memberOfGroup in $gmsaAccount.MemberOf)
                            {
                                try
                                {
                                    $groupObject = Get-ADGroup -SearchBase $usersOuPath -Filter {Name -eq $memberOfGroup} @serverParams
                                }
                                catch {}

                                if ($groupObject)
                                {
                                    try
                                    {
                                        $isMemberOfThisGroup = Get-ADGroupMember -Identity $groupObject @serverParams | Where-Object {$_.SID -eq $($adGmsaAccount.SID)}
                                    }
                                    catch {
                                        $isMemberOfThisGroup = $false
                                    }

                                    if (-not $isMemberOfThisGroup)
                                    {
                                        Log-Info -Message (" Missing GMSA account ({0}) membership: {1}" -f $gmsaName,$memberOfGroup) -Type Warning -Function "gMSAsExist"
                                        $isMember = $false
                                    }
                                } else {
                                    Log-Info -Message (" Missing SG '{0}' expected for to contain GMSA account '{1}'" -f $memberOfGroup,$gmsaName) -Type Warning -Function "gMSAsExist"
                                    $isMember = $false
                                }
                            }
                        }
                        catch {}

                        $accountMissing = -not $isMember
                    }
                }
                catch {}

                if ($accountMissing)
                {
                    $missingGmsaAccounts += $gmsaAccount.GmsaName
                }
            }

            return New-Object PSObject -Property @{
                Resource    = "GmsaAccounts"
                Status      = if ($missingGmsaAccounts.Count -eq 0) { 'Succeeded' } else { 'Failed' }
                TimeStamp   = [datetime]::UtcNow
                Source      = $ENV:COMPUTERNAME
                Detail = ($testContext["LcAdTxt"].GmsaAccountsMissingRemediation -f ($missingGmsaAccounts -join ', '))
            }
        }
    }),
    (New-Object -Type ExternalADTest -Property @{
        TestName = "GroupMembershipsExist"
        ExecutionBlock = {
            Param ([hashtable]$testContext)

            $usersOuPath = $testContext["UsersADOUPath"]

            $serverParams = @{}
            if ($TestContext["AdServer"])
            {
                $serverParams += @{Server = $TestContext["AdServer"]}
            }
            if ($TestContext["AdCredentials"])
            {
                $serverParams += @{Credential = $TestContext["AdCredentials"]}
            }

            $SecurityGroupMemberships = @(
                [pscustomobject]@{ Name="$($testContext["NamingPrefix"])-HA-R-SrvSG";
                                   MemberOf=@("$($testContext["NamingPrefix"])-Hc-Rs-SrvSG", "$($testContext["NamingPrefix"])-IH-HsSG",  "$($testContext["NamingPrefix"])-IH-MsSG", "$($testContext["NamingPrefix"])-FsAcl-InfraSG");
                                   MissingMemberships=@()}
            )

            $results = @()

            foreach ($securityGroupMembership in $SecurityGroupMemberships)
            {
                $sgName = $securityGroupMembership.Name
                $sgMemberList = $securityGroupMembership.MemberOf
                $groupObject = $null
                try
                {
                    $groupObject = Get-ADGroup -SearchBase $usersOuPath -Filter {Name -eq $sgName} @serverParams
                }
                catch {
                    Log-Info -Message (" Failed to get AD security group '{0}'. Inner exception: {1}" -f $sgName,$_) -Type Error -Function "GroupMembershipsExist"
                }

                if ($groupObject)
                {
                    foreach ($securityGroupName in $sgMemberList)
                    {
                        $isMember = $false
                        try
                        {
                            $parentGroupObject = Get-ADGroup -SearchBase $usersOuPath -Filter {Name -eq $securityGroupName} @serverParams

                            if ($groupObject)
                            {
                                $isMember = Get-ADGroupMember -Identity $parentGroupObject @serverParams | Where-Object {$_.SID -eq $($groupObject.SID)}
                            }
                        }
                        catch {
                            Log-Info -Message (" Failed to determine whether security group '{0}' includes security group '{1}'. Inner exception: {1}" -f $securityGroupName,$sgName,$_) -Type Error -Function "GroupMembershipsExist"
                        }

                        if (-not $isMember)
                        {
                            Log-Info -Message (" FAILED: Security group '{0}' DOES NOT include security group '{1}'." -f $securityGroupName,$sgName) -Type Warning -Function "GroupMembershipsExist"
                            $securityGroupMembership.MissingMemberships += $securityGroupName
                        }
                    }
                }
                else {
                    $securityGroupMembership.MissingMemberships = $sgMemberList
                }

                $results +=  New-Object PSObject -Property @{
                    Resource    = "NestedSecurityGroups_$sgName"
                    Status      = if ($securityGroupMembership.MissingMemberships.Count -eq 0) { 'Succeeded' } else { 'Failed' }
                    TimeStamp   = [datetime]::UtcNow
                    Source      = $ENV:COMPUTERNAME
                    Detail = ($testContext["LcAdTxt"].NestedSecurityGroupsMissingRemediation -f $sgName,($securityGroupMembership.MissingMemberships -join ', '))
                }
            }

            return $results
        }
    }),
    (New-Object -Type ExternalADTest -Property @{
        TestName = "GpoInheritanceIsBlocked"
        ExecutionBlock = {
            Param ([hashtable]$testContext)

            $serverParams = @{}
            if ($TestContext["AdServer"])
            {
                $serverParams += @{Server = $TestContext["AdServer"]}
            }
            $ouList = @($testContext["ADOUPath"],$testContext["ComputersADOUPath"],$testContext["UsersADOUPath"])

            $ousWithoutGpoInheritanceBlocked = @()

            $accessWasDenied = $false

            try
            {
                foreach ($ouItem in $ouList)
                {
                    try
                    {
                        $gpInheritance = Get-GPInheritance -Target $ouItem @serverParams
                    }
                    catch
                    {
                        if ($_.Exception -is [System.DirectoryServices.ActiveDirectory.ActiveDirectoryOperationException]) { throw }
                    }

                    if ((-not $gpInheritance) -or (-not $gpInheritance.GpoInheritanceBlocked))
                    {
                        $ousWithoutGpoInheritanceBlocked += $ouItem
                    }
                }
            }
            catch [System.DirectoryServices.ActiveDirectory.ActiveDirectoryOperationException]
            {
                $accessWasDenied = $true
            }

            $statusValue = 'Succeeded'
            if ($ousWithoutGpoInheritanceBlocked.Count -ne 0)
            {
                $statusValue = 'Failed'
            }
            if ($accessWasDenied)
            {
                $statusValue = 'Skipped'
            }

            return New-Object PSObject -Property @{
                Resource    = "OuGpoInheritance"
                Status      = $statusValue
                TimeStamp   = [datetime]::UtcNow
                Source      = $ENV:COMPUTERNAME
                Detail = $testContext["LcAdTxt"].OuInheritanceBlockedMissingRemediation
            }
        }
    }),
    (New-Object -Type ExternalADTest -Property @{
        TestName = "ExecutingAsDeploymentUser"
        ExecutionBlock = {

            <#
            During deployment, the environment checker itself runs as a local admin account, but we need to make sure that the AD credentials that are passed in
            meet all the same criteria as if they were created as part of the AD pre creation tool script.
            As a result, the user specified with these credentials needs to have:
            * All access to the deployment OU in AD
            * Membership to a set of SGs as mentioned in the array below
            #>


            Param ([hashtable]$testContext)

            # Values retrieved from the test context
            $adOuPath = $testContext["ADOUPath"]
            $namingPrefix = $testContext["NamingPrefix"]
            $usersOuPath = $testContext["UsersADOUPath"]
            [pscredential]$credentials = $testContext["AdCredentials"]
            $credentialName = $null

            if ($credentials)
            {
                # Get the user SID so we can find it in the ACL
                $credentialParts = $credentials.UserName.Split("\\")
                $credentialName = $credentialParts[$credentialParts.Length-1]
            }
            else
            {
                $credentialName = $env:USERNAME
            }

            $serverParams = @{}
            if ($TestContext["AdServer"])
            {
                $serverParams += @{Server = $TestContext["AdServer"]}
            }
            if ($TestContext["AdCredentials"])
            {
                $serverParams += @{Credential = $TestContext["AdCredentials"]}
            }

            # Defined set of SGs that need to have the AD user as a member. This needs to be kept in sync with
            # the list at the bottom of AsHciADArtifactsPreCreationTool.psm1 :: New-AsHciSecurityGroup
            $requiredSgMemberships = @(
                "$($namingPrefix)-OpsAdmin",
                "$($namingPrefix)-EceSG",
                "$($namingPrefix)-BM-ECESG",
                "$($namingPrefix)-FsAcl-InfraSG",
                "$($namingPrefix)-DLSG",
                "Domain Users"
            )

            $deprecatedSgMemberships = @(
                "$($namingPrefix)-FsAcl-AcsSG"
            )

            $userSID = $null
            try {
                $userSecurityIdentifier = Get-ADUser -Filter {Name -eq $credentialName} -SearchBase $adOuPath @serverParams
                if ($userSecurityIdentifier) {
                    $userSID = [System.Security.Principal.SecurityIdentifier] $userSecurityIdentifier.SID
                }
            }
            catch {
                Log-Info -Message (" Failed to get user '{0}' in Active Directory. Inner exception: {1}" -f $credentialName,$_) -Type Error -Function "ExecutingAsDeploymentUser"
            }

            if (-not $userSID)
            {
                return New-Object PSObject -Property @{
                    Resource    = "ExecutingAsDeploymentUser"
                    Status      = "Failed"
                    TimeStamp   = [datetime]::UtcNow
                    Source      = $ENV:COMPUTERNAME
                    Detail = ($testContext["LcAdTxt"].ADUserNotFound -f $credentials.UserName,$adOuPath)
                }
            }
            else
            {
                Log-Info -Message (" Found user '{0}' in Active Directory" -f $credentialName) -Type Info -Function "ExecutingAsDeploymentUser"

                # Test whether the AdCredentials user has all access rights to the OU
                $userHasOuPermissions = $false
                try {

                    # The AD module SHOULD install a drive that we can use to get ACLs. However, sometimes it isn't properly registered
                    # especially if we just installed it. So verify that it's usable
                    $adDriveName = "AD"
                    $tempDriveName = "hciad"
                    $adDriveObject = $null

                    try
                    {
                        $adProvider = Get-PSProvider -PSProvider ActiveDirectory
                        if ($adProvider -and $adProvider.Drives.Count -gt 0)
                        {
                            $adDriveObject = $adProvider.Drives | Where-Object {$_.Name -eq $adDriveName -or $_.Name -eq $tempDriveName}
                        }
                    }
                    catch {
                        Log-Info -Message (" Error while trying to access active directory PS drive. Will fall back to creating a new PS drive. Inner exception: {0}" -f $_) -Type Warning -Function "ExecutingAsDeploymentUser"
                    }

                    if (-not $adDriveObject)
                    {
                        try {
                            # Add a new drive
                            $adDriveObject = New-PSDrive -Name $tempDriveName -PSProvider ActiveDirectory -Root '' @serverParams
                        }
                        catch {
                            Log-Info -Message (" Error while trying to create active directory PS drive. Inner exception: {0}" -f $_) -Type Error -Function "ExecutingAsDeploymentUser"
                        }
                    }

                    $ouAcl = $null

                    if ($adDriveObject)
                    {
                        $adDriveName = $adDriveObject.Name

                        try
                        {
                            $ouPath = ("{0}:\{1}" -f $adDriveName,$adOuPath)
                            $ouAcl = Get-Acl $ouPath
                        }
                        catch
                        {
                            Log-Info -Message (" Can't get acls from {0}. Inner exception: {1}" -f $ouPath,$_) -Type Error -Function "ExecutingAsDeploymentUser"
                        }
                        finally {
                            # best effort cleanup if we had added the temp drive
                            try
                            {
                                if ($adDriveName -eq $tempDriveName)
                                {
                                    $adDriveObject | Remove-PSDrive
                                }
                            }
                            catch {}
                        }
                    }

                    if ($ouAcl) {
                        try {
                            # must specify the type to retrieve -- need to get something comparable to the userSID
                            $accessRules = $ouAcl.GetAccessRules($true, $true, $userSID.GetType())
                        }
                        catch {
                            Log-Info -Message (" Error while trying to get access rules for OU. Inner exception: {0}" -f $_) -Type Error -Function "ClusterExistsWithRequiredAcl"
                        }

                        # Check that the GenericAll ACE has been added
                        $genericAllAce = $accessRules | Where-Object { `
                            $_.IdentityReference -eq $userSID -and `
                            $_.ActiveDirectoryRights -bor [System.DirectoryServices.ActiveDirectoryRights]::GenericAll -and `
                            $_.AccessControlType -eq [System.Security.AccessControl.AccessControlType]::Allow -and `
                            $_.InheritanceType -eq [System.DirectoryServices.ActiveDirectorySecurityInheritance]::All
                        }

                        if ($genericAllAce)
                        {
                            $userHasOuPermissions = $true
                        }
                        else
                        {
                            Log-Info -Message (" Found ACLs for AD OU ({0}), but user ({1})'s SID ({2}) not granted GenericAll access" -f $ouPath,$credentialName,$userSID) -Type Warning -Function "ExecutingAsDeploymentUser"
                        }
                    }
                }
                catch {
                    Log-Info -Message (" FAILED to look up ACL for AD OU ({0}) and search for GenericAll ACE for user ({1}). Inner exception: {2}" -f $ouPath,$credentialName,$_) -Type Error -Function "ExecutingAsDeploymentUser"
                }

                $missingSgMemberships = @()
                foreach ($requiredSgName in $requiredSgMemberships)
                {
                    try {
                        $groupObject = Get-ADGroup -SearchBase $usersOuPath -Filter {Name -eq $requiredSgName} @serverParams
                    }
                    catch {
                        Log-Info -Message (" FAILED to look up required SG ({0}). Inner exception: {1}" -f $requiredSgName,$_) -Type Error -Function "ExecutingAsDeploymentUser"
                    }

                    # If the group doesn't exist we report in a different test
                    if ($groupObject) {
                        try
                        {
                            $adGroupMemberEntry = Get-ADGroupMember -Identity $groupObject | Where-Object {$_.SID -eq $userSID}
                        }
                        catch {
                            Log-Info -Message (" FAILED to look up user SID in required SG membership ({0}). Inner exception: {1}" -f $requiredSgName,$_) -Type Error -Function "ExecutingAsDeploymentUser"
                        }


                        if (-not $adGroupMemberEntry)
                        {
                            $missingSgMemberships += $requiredSgName
                            Log-Info -Message (" User {0} not a member of the required security group: {1}" -f $credentialName,$requiredSgName) -Type Warning -Function "ExecutingAsDeploymentUser"
                        }
                    }
                }

                # Find all the AD groups, and search for any that contain the deployment user (especially domain admin!)
                $extraGroups = @()
                $failureReasons = @()

                try {
                    $allGroups = Get-ADGroup -Filter '*' @serverParams
                }
                catch {
                    $failureReasons += ($testContext["LcAdTxt"].ActiveDirectoryError -f "Get-ADGroup",$_)
                    Log-Info -Message (" FAILED to get all AD security groups. Inner exception: {0}" -f $_) -Type Error -Function "ExecutingAsDeploymentUser"
                    $allGroups = @()
                }

                foreach ($singleGroupObject in $allGroups)
                {
                    if (-not ($requiredSgMemberships -contains $singleGroupObject.Name) -and -not ($deprecatedSgMemberships -contains $singleGroupObject.Name))
                    {
                        try
                        {
                            $foundAdUser = Get-ADGroupMember -Identity $singleGroupObject | Where-Object {$_.SID -eq $userSID}
                        }
                        catch
                        {
                            # This can actually fail for a number of reasons that should not block deployment, including one-way trust issues, etc.
                            # For now, simply ignore any group that we enumerated with Get-ADGroup but can't find membership info on.
                            $foundAdUser = $null
                        }


                        if ($foundAdUser)
                        {
                            $extraneousGroupName = $singleGroupObject.DistinguishedName
                            $extraGroups += $extraneousGroupName
                            Log-Info -Message (" User {0} should not be a member of additional security group: {1}" -f $credentialName,$extraneousGroupName) -Type Warning -Function "ExecutingAsDeploymentUser"
                        }
                    }
                }

                # Summarize detail based on what failed
                if (-not $userHasOuPermissions) {
                    $failureReasons += ($testContext["LcAdTxt"].CurrentUserMissingOUAccess -f $adOuPath)
                }

                if ($missingSgMemberships.Count -gt 0) {
                    $missingGroupMembershipList = $missingSgMemberships -join ", "
                    $failureReasons += ($testContext["LcAdTxt"].CurrentUserMissingSGMembership -f $missingGroupMembershipList)
                }

                if ($extraGroups.Count -gt 0) {
                    $extraneousGroupMembershipList = $extraGroups -join ", "
                    $failureReasons += ($testContext["LcAdTxt"].CurrentUserHasExcessSGMemberships -f $extraneousGroupMembershipList)
                }

                if ($failureReasons.Count -gt 0) {
                    $statusValue = 'Failed'
                    $allFailureReasons = $failureReasons -join "; "
                    $detail = ($testContext["LcAdTxt"].CurrentUserFailureSummary -f $credentials.UserName,$allFailureReasons)
                }
                else
                {
                    $statusValue = 'Succeeded'
                    $detail = ""
                }

                return New-Object PSObject -Property @{
                    Resource    = "ExecutingAsDeploymentUser"
                    Status      = $statusValue
                    TimeStamp   = [datetime]::UtcNow
                    Source      = $ENV:COMPUTERNAME
                    Detail      = $detail
                }
            }
        }
    })
)

function Test-OrganizationalUnitOnSession {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $ADOUPath,

        [Parameter(Mandatory=$true)]
        [string]
        $DomainFQDN,

        [Parameter(Mandatory=$true)]
        [string]
        $NamingPrefix,

        [Parameter(Mandatory=$true)]
        [string]
        $ClusterName,

        [Parameter(Mandatory)]
        [array]
        $PhysicalMachineNames,

        [Parameter(Mandatory=$false)]
        [System.Management.Automation.Runspaces.PSSession]
        $Session,

        [Parameter(Mandatory=$false)]
        [string]
        $ActiveDirectoryServer,

        [Parameter(Mandatory=$false)]
        [pscredential]
        $ActiveDirectoryCredentials
    )

    $testContext = @{
        ADOUPath = $ADOUPath
        ComputersADOUPath = "OU=Computers,$ADOUPath"
        UsersADOUPath = "OU=Users,$ADOUPath"
        DomainFQDN = $DomainFQDN
        NamingPrefix = $NamingPrefix
        ClusterName = $ClusterName
        LcAdTxt = $lcAdTxt
        AdServer = $ActiveDirectoryServer
        AdCredentials = $ActiveDirectoryCredentials
        AdCredentialsUserName = if ($ActiveDirectoryCredentials) { $ActiveDirectoryCredentials.UserName } else { "" }
        PhysicalMachineNames = $PhysicalMachineNames
    }

    $computerName = if ($Session) { $Session.ComputerName } else { $ENV:COMPUTERNAME }

    Log-Info -Message "Executing test on $computerName" -Type Info

    # Reuse the parameters for Invoke-Command so that we only have to set up context and session data once
    $invokeParams = @{
        ScriptBlock = $null
        ArgumentList = $testContext
    }
    if ($Session) {
        $invokeParams += @{Session = $Session}
    }

    # If provided, verify the AD server and credentials are reachable
    if ($ActiveDirectoryServer -or $ActiveDirectoryCredentials)
    {
        $params = @{}
        if ($ActiveDirectoryServer)
        {
            $params["Server"] = $ActiveDirectoryServer
        }
        if ($ActiveDirectoryCredentials)
        {
            $params["Credential"] = $ActiveDirectoryCredentials
        }
        try {
            Get-ADDomain @params
        }
        catch {
            if (-not $ActiveDirectoryServer) {
                $ActiveDirectoryServer = "default"
            }
            $userName = "default"
            if ($ActiveDirectoryCredentials) {
                $userName = $ActiveDirectoryCredentials.UserName
            }
            throw ("Unable to contact AD server {0} using {1} credentials. Internal exception: {2}" -f $ActiveDirectoryServer,$userName,$_)
        }
    }

    # Initialize the array of detailed results
    $detailedResults = @()

    # Test preparation -- fill in more of the test context that needs to be executed remotely
    $ExternalAdTestInitializors | ForEach-Object {
        $invokeParams.ScriptBlock = $_.ExecutionBlock
        $testName = $_.TestName

        Log-Info -Message "Executing test initializer $testName" -Type Info

        try
        {
            $results = Invoke-Command @invokeParams

            if ($results)
            {
                $testContext += $results
            }
        }
        catch {
            throw ("Unable to execute test {0} on {1}. Inner exception: {2}" -f $testName,$computerName,$_)
        }
    }

    Log-Info -Message "Executing tests with parameters: " -Type Info
    foreach ($key in $testContext.Keys)
    {
        if ($key -ne "LcAdTxt")
        {
            Log-Info -Message " $key : $($testContext[$key])" -Type Info
        }
    }

    # Update InvokeParams with the full context
    $invokeParams.ArgumentList = $testContext

    # For each test, call the test execution block and append the results
    $ExternalAdTests | ForEach-Object {
        # override ScriptBlock with the particular test execution block
        $invokeParams.ScriptBlock = $_.ExecutionBlock
        $testName = $_.TestName

        Log-Info -Message "Executing test $testName" -Type Info

        try
        {
            $results = Invoke-Command @invokeParams

            Log-Info -Message ("Test $testName completed with: {0}" -f $results) -Type Info

            $detailedResults += $results
        }
        catch {
            Log-Info -Message ("Test $testName FAILED. Inner exception: {0}" -f $_) -Type Info
        }
    }

    return $detailedResults
}

function Test-OrganizationalUnit {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $ADOUPath,

        [Parameter(Mandatory=$true)]
        [string]
        $DomainFQDN,

        [Parameter(Mandatory=$true)]
        [string]
        $NamingPrefix,

        [Parameter(Mandatory=$true)]
        [string]
        $ClusterName,

        [Parameter(Mandatory=$true)]
        [array]
        $PhysicalMachineNames,

        [Parameter(Mandatory=$false)]
        [System.Management.Automation.Runspaces.PSSession]
        $PsSession,

        [Parameter(Mandatory=$false)]
        [string]
        $ActiveDirectoryServer = $null,

        [Parameter(Mandatory=$false)]
        [pscredential]
        $ActiveDirectoryCredentials = $null
    )

    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop

    Log-Info -Message "Executing Test-OrganizationalUnit"
    $fullTestResults = Test-OrganizationalUnitOnSession -ADOUPath $ADOUPath -DomainFQDN $DomainFQDN -NamingPrefix $NamingPrefix -ClusterName $ClusterName -Session $PsSession -ActiveDirectoryServer $ActiveDirectoryServer -ActiveDirectoryCredentials $ActiveDirectoryCredentials -PhysicalMachineNames $PhysicalMachineNames

    # Build the results
    $now = [datetime]::UtcNow
    $TargetComputerName = if ($PsSession.PSComputerName) { $PsSession.PSComputerName } else { $ENV:COMPUTERNAME }
    $aggregateStatus = if ($fullTestResults.Status -notcontains 'Failed') { 'Succeeded' } else { 'Failed' }
    $remediationValues = $fullTestResults | Where-Object -Property Status -NE 'Succeeded' | Select-Object $Remediation
    $remediationValues = $remediationValues -join "`r`n"
    if (-not $remediationValues)
    {
        $remediationValues = ''
    }
    $testOuResult = New-Object -Type OrganizationalUnitTestResult -Property @{
        Name               = 'AzStackHci_ExternalActiveDirectory_Test_OrganizationalUnit'
        Title              = 'Test AD Organizational Unit'
        Severity           = 'Critical'
        Description        = 'Tests that the specified organizational unit exists and contains the proper sub-OUs'
        Tags               = $null
        Remediation        = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-active-directory'
        TargetResourceID   = "Test_AD_OU_$TargetComputerName"
        TargetResourceName = "Test_AD_OU_$TargetComputerName"
        TargetResourceType = 'ActiveDirectory'
        Timestamp          = $now
        Status             = $aggregateStatus
        AdditionalData     = $fullTestResults
        HealthCheckSource  = $ENV:EnvChkrId
    }
    return $testOuResult
}
# SIG # Begin signature block
# MIIoLQYJKoZIhvcNAQcCoIIoHjCCKBoCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCC4WcvgbV6kiakE
# lS32FsyWb0ckzYoB4MlbKKghhM1iZaCCDXYwggX0MIID3KADAgECAhMzAAADTrU8
# esGEb+srAAAAAANOMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjMwMzE2MTg0MzI5WhcNMjQwMzE0MTg0MzI5WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQDdCKiNI6IBFWuvJUmf6WdOJqZmIwYs5G7AJD5UbcL6tsC+EBPDbr36pFGo1bsU
# p53nRyFYnncoMg8FK0d8jLlw0lgexDDr7gicf2zOBFWqfv/nSLwzJFNP5W03DF/1
# 1oZ12rSFqGlm+O46cRjTDFBpMRCZZGddZlRBjivby0eI1VgTD1TvAdfBYQe82fhm
# WQkYR/lWmAK+vW/1+bO7jHaxXTNCxLIBW07F8PBjUcwFxxyfbe2mHB4h1L4U0Ofa
# +HX/aREQ7SqYZz59sXM2ySOfvYyIjnqSO80NGBaz5DvzIG88J0+BNhOu2jl6Dfcq
# jYQs1H/PMSQIK6E7lXDXSpXzAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUnMc7Zn/ukKBsBiWkwdNfsN5pdwAw
# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW
# MBQGA1UEBRMNMjMwMDEyKzUwMDUxNjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci
# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG
# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0
# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAD21v9pHoLdBSNlFAjmk
# mx4XxOZAPsVxxXbDyQv1+kGDe9XpgBnT1lXnx7JDpFMKBwAyIwdInmvhK9pGBa31
# TyeL3p7R2s0L8SABPPRJHAEk4NHpBXxHjm4TKjezAbSqqbgsy10Y7KApy+9UrKa2
# kGmsuASsk95PVm5vem7OmTs42vm0BJUU+JPQLg8Y/sdj3TtSfLYYZAaJwTAIgi7d
# hzn5hatLo7Dhz+4T+MrFd+6LUa2U3zr97QwzDthx+RP9/RZnur4inzSQsG5DCVIM
# pA1l2NWEA3KAca0tI2l6hQNYsaKL1kefdfHCrPxEry8onJjyGGv9YKoLv6AOO7Oh
# JEmbQlz/xksYG2N/JSOJ+QqYpGTEuYFYVWain7He6jgb41JbpOGKDdE/b+V2q/gX
# UgFe2gdwTpCDsvh8SMRoq1/BNXcr7iTAU38Vgr83iVtPYmFhZOVM0ULp/kKTVoir
# IpP2KCxT4OekOctt8grYnhJ16QMjmMv5o53hjNFXOxigkQWYzUO+6w50g0FAeFa8
# 5ugCCB6lXEk21FFB1FdIHpjSQf+LP/W2OV/HfhC3uTPgKbRtXo83TZYEudooyZ/A
# Vu08sibZ3MkGOJORLERNwKm2G7oqdOv4Qj8Z0JrGgMzj46NFKAxkLSpE5oHQYP1H
# tPx1lPfD7iNSbJsP6LiUHXH1MIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq
# hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5
# IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg
# Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
# CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03
# a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr
# rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg
# OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy
# 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9
# sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh
# dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k
# A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB
# w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn
# Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90
# lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w
# ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o
# ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD
# VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa
# BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny
# bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG
# AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV
# HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG
# AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl
# AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb
# C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l
# hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6
# I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0
# wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560
# STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam
# ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa
# J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah
# XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA
# 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt
# Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr
# /Xmfwb1tbWrJUnMTDXpQzTGCGg0wghoJAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBIDIwMTECEzMAAANOtTx6wYRv6ysAAAAAA04wDQYJYIZIAWUDBAIB
# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIApXxOiu1/lFDJr3V8HRVJdq
# 1Xm01CviEfVXg19HUuyCMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A
# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB
# BQAEggEAqROXeN/XoKCEbLUkIINZtExEl953AqsQseQzANBsXY07Cy56HNTH8wBn
# feM0P05kqirmQP9WJD+UOcMqn7M8Jy9dKBUbnM3xsEKbCdfq347Jepmsl12EeIN/
# x+3hNsiKS9hOkkl5IK1sJ4zGH3h1xjK1q+Eih2bG9mOd/4xurr1iakm6jCO8+LoH
# 97TwpiXsz6a9xJoQJkaUTnPwwP+kDy8bzpr4hM3I/k/aFiStwGni2k08pR8suBwB
# aeSm+Q8mYHb0p7AZhWjPk9QzKwx/pPnYHVKG3/qQr63Vb9sRwdHjZcuMgNE79IyP
# RqLNI47GDSwNVMaHXK85QmVHFrbS+aGCF5cwgheTBgorBgEEAYI3AwMBMYIXgzCC
# F38GCSqGSIb3DQEHAqCCF3AwghdsAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFSBgsq
# hkiG9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl
# AwQCAQUABCDiIfj57/hu1si3CVWvWFBeUmgNqGzByJDFEb18pCfdcAIGZQPkWMPD
# GBMyMDIzMDkyMjA4MzEwNS4yNTNaMASAAgH0oIHRpIHOMIHLMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1l
# cmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046OTIwMC0w
# NUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2Wg
# ghHtMIIHIDCCBQigAwIBAgITMwAAAc9SNr5xS81IygABAAABzzANBgkqhkiG9w0B
# AQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD
# VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0yMzA1MjUxOTEy
# MTFaFw0yNDAyMDExOTEyMTFaMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz
# aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv
# cnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25z
# MScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046OTIwMC0wNUUwLUQ5NDcxJTAjBgNV
# BAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQC4Pct+15TYyrUje553lzBQodgmd5Bz7WuH8SdHpAoW
# z+01TrHExBSuaMKnxvVMsyYtas5h6aopUGAS5WKVLZAvUtH62TKmAE0JK+i1hafi
# CSXLZPcRexxeRkOqeZefLBzXp0nudMOXUUab333Ss8LkoK4l3LYxm1Ebsr3b2OTo
# 2ebsAoNJ4kSxmVuPM7C+RDhGtVKR/EmHsQ9GcwGmluu54bqiVFd0oAFBbw4txTU1
# mruIGWP/i+sgiNqvdV/wah/QcrKiGlpWiOr9a5aGrJaPSQD2xgEDdPbrSflYxsRM
# dZCJI8vzvOv6BluPcPPGGVLEaU7OszdYjK5f4Z5Su/lPK1eST5PC4RFsVcOiS4L0
# sI4IFZywIdDJHoKgdqWRp6Q5vEDk8kvZz6HWFnYLOlHuqMEYvQLr6OgooYU9z0A5
# cMLHEIHYV1xiaBzx2ERiRY9MUPWohh+TpZWEUZlUm/q9anXVRN0ujejm6OsUVFDs
# sIMszRNCqEotJGwtHHm5xrCKuJkFr8GfwNelFl+XDoHXrQYL9zY7Np+frsTXQpKR
# NnmI1ashcn5EC+wxUt/EZIskWzewEft0/+/0g3+8YtMkUdaQE5+8e7C8UMiXOHkM
# K25jNNQqLCedlJwFIf9ir9SpMc72NR+1j6Uebiz/ZPV74do3jdVvq7DiPFlTb92U
# KwIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFDaeKPtp0eTSVdG+gZc5BDkabTg4MB8G
# A1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCG
# Tmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUy
# MFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4w
# XAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2Vy
# dHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwG
# A1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQD
# AgeAMA0GCSqGSIb3DQEBCwUAA4ICAQBQgm4pnA0xkd/9uKXJMzdMYyxUfUm/ZusU
# Ba32MEZXQuMGp20pSuX2VW9/tpTMo5bkaJdBVoUyd2DbDsNb1kjr/36ntT0jvL3A
# oWStAFhZBypmpPbx+BPK49ZlejlM4d5epX668tRRGfFip9Til9yKRfXBrXnM/q64
# IinN7zXEQ3FFQhdJMzt8ibXClO7eFA+1HiwZPWysYWPb/ZOFobPEMvXie+GmEbTK
# bhE5tze6RrA9aejjP+v1ouFoD5bMj5Qg+wfZXqe+hfYKpMd8QOnQyez+Nlj1ityn
# OZWfwHVR7dVwV0yLSlPT+yHIO8g+3fWiAwpoO17bDcntSZ7YOBljXrIgad4W4gX+
# 4tp1eBsc6XWIITPBNzxQDZZRxD4rXzOB6XRlEVJdYZQ8gbXOirg/dNvS2GxcR50Q
# dOXDAumdEHaGNHb6y2InJadCPp2iT5QLC4MnzR+YZno1b8mWpCdOdRs9g21QbbrI
# 06iLk9KD61nx7K5ReSucuS5Z9nbkIBaLUxDesFhr1wmd1ynf0HQ51Swryh7YI7TX
# T0jr81mbvvI9xtoqjFvIhNBsICdCfTR91ylJTH8WtUlpDhEgSqWt3gzNLPTSvXAx
# XTpIM583sZdd+/2YGADMeWmt8PuMce6GsIcLCOF2NiYZ10SXHZS5HRrLrChuzedD
# RisWpIu5uTCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZI
# hvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# MjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAy
# MDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp
# bWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
# AQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25Phdg
# M/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPF
# dvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6
# GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBp
# Dco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50Zu
# yjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3E
# XzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0
# lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1q
# GFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ
# +QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PA
# PBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkw
# EgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxG
# NSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARV
# MFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWlj
# cm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAK
# BggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMC
# AYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvX
# zpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20v
# cGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYI
# KwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG
# 9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0x
# M7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmC
# VgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449
# xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wM
# nosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDS
# PeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2d
# Y3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxn
# GSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+Crvs
# QWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokL
# jzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL
# 6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggNQ
# MIICOAIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp
# bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw
# b3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEn
# MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjkyMDAtMDVFMC1EOTQ3MSUwIwYDVQQD
# ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQDq
# 8xzVXwLguauAQj1rrJ4/TyEMm6CBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w
# IFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA6LedETAiGA8yMDIzMDkyMjA0NTY0
# OVoYDzIwMjMwOTIzMDQ1NjQ5WjB3MD0GCisGAQQBhFkKBAExLzAtMAoCBQDot50R
# AgEAMAoCAQACAgwuAgH/MAcCAQACAhQxMAoCBQDouO6RAgEAMDYGCisGAQQBhFkK
# BAIxKDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJ
# KoZIhvcNAQELBQADggEBAE40Ypp1XzCgGb7WdgwwKXpy4leusnN9apOq56K4r2tM
# f4ameojoBYpxwIUwEvP7wDaLBM7DGn0VeN3hrY3y5ZuFdbp+B1b1iks+U4PFg5IS
# uaOmJzpMBp7lzMiGnrMnDZV77p/Bcx5Y1QzM0fQjydi/SvGb0Reicz+cPZx0H+2C
# li0jzwHwXXu57PmhpwCwm1lWyYHC2hqFjY6ENU3h1ArB2zO2D2GzPhLJDP3MGkNA
# sJGZ35rWz1TQNWSmSeRebdEU9HxYenYWab1xNY7weggwmWI+wRVjQ8/D/FH4uk6o
# nJ5E+crkJCCm0nTWc5kyXRUY0jke4kK49YIeAzm3bGoxggQNMIIECQIBATCBkzB8
# MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVk
# bW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1N
# aWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAc9SNr5xS81IygABAAAB
# zzANBglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEE
# MC8GCSqGSIb3DQEJBDEiBCAUcEoQ6AXCIc2Eobdi+zbjmcA0by0rWrpmGg4AXuP/
# VTCB+gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EILPpsLqeNS4NuYXE2VJlMuvQ
# eWVA80ZDFhpOPjSzhPa/MIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
# Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENB
# IDIwMTACEzMAAAHPUja+cUvNSMoAAQAAAc8wIgQgwYErpH3GWsKS0Phe/FGbIvgq
# zcTrp0DwEm5R13B+KIMwDQYJKoZIhvcNAQELBQAEggIAP91a2MmHlc+OM4iq4i2q
# k9WUf2BNQZoXmf+Ej6h59/pMkK8lozVbNSpwZpTR2DYyEstvbxzODPSEeisHG47X
# 0MT1jPINuNNgLwj91DK5aHHrzbsd0DrRFW+DLVrCJijQTtV3FmAPGEiXZr1R3/LE
# zsSQPkAHMOMD28Gs9uMLBlOdWEjinF4iFKHyO6RR1a4gOJw07GOdpvPB4IG0TBgc
# hpNbgaqrgd/hyk1Tv7vChIP2Edtv7lHuJK6JU13WMMeV6V/0IHSmSf8tp5QQm081
# m1Zh1VxTXUhGXNMSaSBgHI8mFj67jW7RjpV4eILcECdDCPjZAwJg7IKAaSZfoAMd
# 1gwO6wljzBCYl/8in3DZsDqWRh/xYQAY+dkYg7+GUiM/pRj9PyZICThok7I9f/1p
# Q1VFrP5zAHO4NHpprXOls+9vLUAJRwWBiRChu/zeEwe1TvJLqnaIdKj2euaANr7S
# gb2RqrG1xS5v66lTHSIoqzms6tEyZW5Fmpm8DC31aB5X5KOA7++rn0SIEcMUoszv
# zUNwrNPHOn4KmAce+XmEhFDvVyIwchpgB8AGZmVN6vSvpqCd8pgkBwvkLT7zyXnb
# YKiwmTFtdOyxvMWe97KMoga3G5kDAamkzuhi5WBKtZl9CSUoyNOy/Cns1p21zBxe
# g7C+behWMwGjxCiD3vQD6Tw=
# SIG # End signature block