Microsoft.AVS.Management.psm1

<# Private Function Import #>
. $PSScriptRoot\AVSGenericUtils.ps1
. $PSScriptRoot\AVSvSANUtils.ps1

<# Download certificate from SAS token url #>
function Get-Certificates {
    Param
    (
        [Parameter(
            Mandatory = $true)]
        [System.Security.SecureString]
        $SSLCertificatesSasUrl
    )

    [string] $CertificatesSASPlainString = ConvertFrom-SecureString -SecureString $SSLCertificatesSasUrl -AsPlainText
    [System.StringSplitOptions] $options = [System.StringSplitOptions]::RemoveEmptyEntries -bor [System.StringSplitOptions]::TrimEntries
    [string[]] $CertificatesSASList = $CertificatesSASPlainString.Split(",", $options)
    Write-Host "Number of Certs passed $($CertificatesSASList.count)"
    if ($CertificatesSASList.count -eq 0) {
        throw "If adding an LDAPS identity source, please ensure you pass in at least one certificate"
    }
    if ($PSBoundParameters.ContainsKey('SecondaryUrl') -and $CertificatesSASList.count -lt 2) {
        throw "If passing in a secondary/fallback URL, ensure that at least two certificates are passed."
    }
    $DestinationFileArray = @()
    $Index = 1
    foreach ($CertSas in $CertificatesSASList) {
        Write-Host "Downloading Cert $Index..."
        $CertDir = $pwd.Path
        $CertLocation = "$CertDir/cert$Index.cer"
        try {
            $Response = Invoke-WebRequest -Uri $CertSas -OutFile $CertLocation
            $StatusCode = $Response.StatusCode
            Write-Host("Certificate downloaded. $StatusCode")
            $DestinationFileArray += $CertLocation
        }
        catch {
            throw "Failed to download certificate #$($Index): $($PSItem.Exception.Message). Ensure the SAS string is still valid"
        }
        $Index = $Index + 1
    }
    Write-Host "Number of certificates downloaded: $($DestinationFileArray.count)"
    return $DestinationFileArray
}

function Get-StoragePolicyInternal {
    Param
    (
        [Parameter(
            Mandatory = $true)]
        $StoragePolicyName
    )
    Write-Host "Getting Storage Policy $StoragePolicyName"
    $VSANStoragePolicies = Get-SpbmStoragePolicy -Namespace "VSAN" -ErrorAction Stop
    $StoragePolicy = Get-SpbmStoragePolicy $StoragePolicyName -ErrorAction Stop
    if ($null -eq $StoragePolicy) {
        Write-Error "Could not find Storage Policy with the name $StoragePolicyName." -ErrorAction Continue
        Write-Error "Available storage policies: $(Get-SpbmStoragePolicy -Namespace "VSAN")" -ErrorAction Stop
    }
    elseif (-not ($StoragePolicy -in $VSANStoragePolicies)) {
        Write-Error "Storage policy $StoragePolicyName is not supported. Storage policies must be in the VSAN namespace" -ErrorAction Continue
        Write-Error "Available storage policies: $(Get-SpbmStoragePolicy -Namespace "VSAN")" -ErrorAction Stop
    }
    return $StoragePolicy, $VSANStoragePolicies
}

function Set-StoragePolicyOnVM {
    Param
    (
        [Parameter(
            Mandatory = $true)]
        $VM,
        [Parameter(
            Mandatory = $true)]
        $VSANStoragePolicies,
        [Parameter(
            Mandatory = $true)]
        $StoragePolicy
    )
    if (-not $(Get-SpbmEntityConfiguration $VM).StoragePolicy -in $VSANStoragePolicies) {
        Write-Error "Modifying storage policy on $($VM.Name) is not supported"
    }
    Write-Host "Setting VM $($VM.Name) storage policy to $($StoragePolicy.Name)..."
    try {
        Set-VM -VM $VM -StoragePolicy $StoragePolicy -ErrorAction Stop -Confirm:$false
        Write-Output "Successfully set the storage policy on VM $($VM.Name) to $($StoragePolicy.Name)"
    }
    catch [VMware.VimAutomation.ViCore.Types.V1.ErrorHandling.InvalidVmConfig] {
        Write-Error "The selected storage policy $($StoragePolicy.Name) is not compatible with $($VM.Name). You may need more hosts: $($PSItem.Exception.Message)"
    }
    catch {
        Write-Error "Was not able to set the storage policy on $($VM.Name): $($PSItem.Exception.Message)"
    }
}

<#
    .Synopsis
     Not Recommended (use New-LDAPSIdentitySource): Add a not secure external identity source (Active Directory over LDAP) for use with vCenter Server Single Sign-On.

    .Parameter Name
     The user-friendly name the external AD will be given in vCenter

    .Parameter DomainName
     Domain name of the external active directory, e.g. myactivedirectory.local

    .Parameter DomainAlias
     Domain alias of the external active directory, e.g. myactivedirectory

    .Parameter PrimaryUrl
     Url of the primary ldap server to attempt to connect to, e.g. ldap://myadserver.local:389

    .Parameter SecondaryUrl
     Optional: Url of the fallback ldap server to attempt to connect to, e.g. ldap://myadserver.local:389

    .Parameter BaseDNUsers
     Base Distinguished Name for users, e.g. "dc=myadserver,dc=local"

    .Parameter BaseDNGroups
     Base Distinguished Name for groups, e.g. "dc=myadserver,dc=local"

    .Parameter Credential
     Credential to login to the LDAP server (NOT cloudadmin) in the form of a username/password credential. Usernames often look like prodAdmins@domainname.com or if the AD is a Microsoft Active Directory server, usernames may need to be prefixed with the NetBIOS domain name, such as prod\AD_Admin

    .Parameter GroupName
     Optional: A group in the customer external identity source to be added to CloudAdmins. Users in this group will have CloudAdmin access. Group name should be formatted without the domain name, e.g. group-to-give-access

    .Example
    # Add the domain server named "myserver.local" to vCenter
    Add-LDAPIdentitySource -Name 'myserver' -DomainName 'myserver.local' -DomainAlias 'myserver' -PrimaryUrl 'ldap://10.40.0.5:389' -BaseDNUsers 'dc=myserver, dc=local' -BaseDNGroups 'dc=myserver, dc=local'
#>

function New-LDAPIdentitySource {
    [CmdletBinding(PositionalBinding = $false)]
    [AVSAttribute(10, UpdatesSDDC = $false)]
    Param
    (
        [Parameter(
            Mandatory = $true,
            HelpMessage = 'User-Friendly name to store in vCenter')]
        [ValidateNotNull()]
        [string]
        $Name,

        [Parameter(
            Mandatory = $true,
            HelpMessage = 'Full DomainName: adserver.local')]
        [ValidateNotNull()]
        [string]
        $DomainName,

        [Parameter(
            Mandatory = $true,
            HelpMessage = 'DomainAlias: adserver')]
        [string]
        $DomainAlias,

        [Parameter(
            Mandatory = $true,
            HelpMessage = 'URL of your AD Server: ldaps://yourserver:636')]
        [ValidateNotNullOrEmpty()]
        [string]
        $PrimaryUrl,

        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Optional: URL of a backup server')]
        [string]
        $SecondaryUrl,

        [Parameter(
            Mandatory = $true,
            HelpMessage = 'BaseDNGroups, "DC=name, DC=name"')]
        [ValidateNotNull()]
        [string]
        $BaseDNUsers,

        [Parameter(
            Mandatory = $true,
            HelpMessage = 'BaseDNGroups, "DC=name, DC=name"')]
        [ValidateNotNull()]
        [string]
        $BaseDNGroups,

        [Parameter(
            Mandatory = $true,
            HelpMessage = "Credential for the LDAP server")]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential,

        [Parameter (
            Mandatory = $false,
            HelpMessage = 'A group in the external identity source to give CloudAdmins access')]
        [string]
        $GroupName
    )

    if (-not ($PrimaryUrl -match '^(ldap:).+((:389)|(:636)|(:3268)|(:3269))$')) {
        Write-Error "PrimaryUrl $PrimaryUrl is invalid. Ensure the port number is 389, 636, 3268, or 3269 and that the url begins with ldap: and not ldaps:" -ErrorAction Stop
    }
    if (($PrimaryUrl -match '^(ldap:).+((:636)|(:3269))$')) {
        Write-Warning "PrimaryUrl $PrimaryUrl is nonstandard. Are you sure you meant to use the 636/3269 port and not the standard ports for LDAP, 389 or 3268? Continuing anyway.."
    }
    if ($PSBoundParameters.ContainsKey('SecondaryUrl') -and (-not ($SecondaryUrl -match '^(ldap:).+((:389)|(:636)|(:3268)|(:3269))$'))) {
        Write-Error "SecondaryUrl $SecondaryUrl is invalid. Ensure the port number is 389, 636, 3268, or 3269 and that the url begins with ldap: and not ldaps:" -ErrorAction Stop
    }
    if (($SecondaryUrl -match '^(ldap:).+((:636)|(:3269))$')) {
        Write-Warning "SecondaryUrl $SecondaryUrl is nonstandard. Are you sure you meant to use the 636/3269 port and not the standard ports for LDAP, 389 or 3268? Continuing anyway.."
    }

    $ExternalIdentitySources = Get-IdentitySource -External -ErrorAction Continue
    if ($null -ne $ExternalIdentitySources) {
        Write-Host "Checking to see if identity source already exists..."
        if ($DomainName.trim() -eq $($ExternalIdentitySources.Name.trim())) {
            Write-Error $($ExternalIdentitySources | Format-List | Out-String) -ErrorAction Continue
            Write-Error "Already have an external identity source with the same name: $($ExternalIdentitySources.Name). If only trying to add a group to this Identity Source, use Add-GroupToCloudAdmins" -ErrorAction Stop
        }
        else {
            Write-Information "$($ExternalIdentitySources | Format-List | Out-String)"
            Write-Information "An identity source already exists, but not for this domain. Continuing to add this one..."
        }
    }
    else {
        Write-Host "No existing external identity sources found."
    }

    $Password = $Credential.GetNetworkCredential().Password
    Write-Host "Adding $DomainName..."
    Add-LDAPIdentitySource `
        -Name $Name `
        -DomainName $DomainName `
        -DomainAlias $DomainAlias `
        -PrimaryUrl $PrimaryUrl `
        -SecondaryUrl $SecondaryUrl `
        -BaseDNUsers $BaseDNUsers `
        -BaseDNGroups $BaseDNGroups `
        -Username $Credential.UserName `
        -Password $Password `
        -ServerType 'ActiveDirectory' -ErrorAction Stop
    $ExternalIdentitySources = Get-IdentitySource -External -ErrorAction Continue
    $ExternalIdentitySources | Format-List | Out-String

    if ($PSBoundParameters.ContainsKey('GroupName')) {
        Write-Host "GroupName passed in: $GroupName"
        Write-Host "Attempting to add group $GroupName to CloudAdmins..."
        Add-GroupToCloudAdmins -GroupName $GroupName -Domain $DomainName -ErrorAction Stop
    }
}

<#
    .Synopsis
     Download certificates from domain controllers and save them to local files
#>

function Get-CertificateFromServerToLocalFile {
    param (
        [Parameter(
            Mandatory = $true)]
        [ValidateNotNull()]
        [string[]]
        $remoteComputers
    )

    $DestinationFileArray = @()
    $exportFolder = $pwd.Path + "/"
    foreach ($computerUrl in $remoteComputers) {
        try {
            Write-Host ("Starting to Download Cert from " + $computerUrl)
            $Command = 'echo "1" | openssl s_client -connect ' + $ResultUrl.Host + ':' + $ResultUrl.Port + ' -showcerts'
            $SSHRes = Invoke-SSHCommand -Command $Command -SSHSession $SSH_Sessions['VC'].Value
            $SSHOutput = $SSHRes.Output | out-string
        }
        catch {
            throw "Failure to download the certificate from $computerUrl. $_"
        }

        if ($SSHOutput -notmatch '(?s)(?<cert>-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----)') {
            throw "The certificate from $computerUrl has an incorrect format"
        }
        else {
            $certs = select-string -inputobject $SSHOutput -pattern "(?s)(?<cert>-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----)" -allmatches
            $cert = $certs.matches[0]
            $exportPath = $exportFolder + ($ResultUrl.Host.split(".")[0]) + ".cer"
            $cert.Value | Out-File $exportPath -Encoding ascii
            $DestinationFileArray += $exportPath
        }
    }
    Write-Host "Number of certificates downloaded: $($DestinationFileArray.count)"
    return $DestinationFileArray
}

<#
    .Synopsis
     Recommended: Debug functionality of all OpenLDAP and Active Directory Identity Sources for use with vCenter Server Single Sign-On.

    .Example
    # Debug the OpenLDAP and Active Directory Identity Sources configured in vCenter
    Debug-LDAPSIdentitySources
#>

function Debug-LDAPSIdentitySources {
    [AVSAttribute(2, UpdatesSDDC = $false)]
    Param()
    Write-Host "*"
    Write-Host "* LDAP Identity Source Diagnostic Test Tool (ldapcheck)"
    Write-Host "* Executed at $((Get-Date).ToUniversalTime())"
    Write-Host "*"

    $sources = Get-IdentitySource -External -ErrorAction Stop
    $sources | ForEach-Object {
        if(($_.Type -eq "OpenLdap") -or ($_.Type -eq "ActiveDirectory")) {

            Write-Host "* -------------------------------------------------------------"
            Write-Host "* OpenLDAP Identity Source $($_.Name) detected."

            $urls = @()
            if(-not ($null -eq $_.PrimaryUrl)) {
                $urls += $_.PrimaryUrl
                Write-Host "* The Primary URL is $($_.PrimaryUrl)."
            }
            if(-not ($null -eq $_.FailoverUrl)) {
                $urls += $_.FailoverUrl
                Write-Host "* The Failover URL is $($_.FailoverUrl)."
            }

            foreach($url in $urls) {
                Write-Host "* Checking LDAP URL: $url"

                # Check URL looks okay:
                # i.e. ldaps://ldap1.ldap.avs.azure.com
                if($url.ToLower() -match '(?<protocol>ldap|ldaps)://(?<hostname>[a-z0-9\.]+)(?<portspec>$|:[0-9]+)') {
                    $ldap_protocol = $Matches.protocol
                    $ldap_hostname = $Matches.hostname
                    $ldap_portspec = $Matches.portspec
                    Write-Host " LDAP Protocol: $ldap_protocol"
                    Write-Host " LDAP Server Hostname: $ldap_hostname"
                    Write-Host " LDAP Port Specified: $ldap_portspec"

                    # Check if the LDAP hostname is in /etc/hosts
                    try {
                        $Command = "grep $ldap_hostname /etc/hosts"
                        $SSHRes = Invoke-SSHCommand -Command $Command -SSHSession $SSH_Sessions['VC'].Value
                    }
                    catch {
                        throw "ERROR: Unable to execute grep command on vCenter."
                    }
                    $SSHOutput = $SSHRes.Output | out-string
                    Write-Host "* vCenter /etc/hosts hostname resolution check returned: $SSHOutput"

                    # Call Host to check DNS resolution of LDAP server from vCenter"
                    try {
                        $Command = "host $ldap_hostname"
                        $SSHRes = Invoke-SSHCommand -Command $Command -SSHSession $SSH_Sessions['VC'].Value
                    }
                    catch {throw "ERROR: Unable to execute host command on vCenter."}
                    Write-Host "* vCenter DNS hostname resolution check returned: $($SSHRes.Output)"

                    # Now let's look at the port numbers
                    if($ldap_portspec -ne "") {
                        $ldap_port = $ldap_portspec -match ":([0-9]+)"
                        Write-Host " LDAP Port number: $ldap_port"
                    } else {
                        switch($ldap_protocol) {
                            "ldap"  {$ldap_port = 389}
                            "ldaps" {$ldap_port = 636}
                            "default" {$ldap_port = -1}
                        }
                        Write-Host "* LDAP Port to test: $ldap_port"
                    }

                    # Call NetCat to test if the LDAP port is open
                    try {
                        $Command = "nc -vz $ldap_hostname $ldap_port"
                        $SSHRes = Invoke-SSHCommand -Command $Command -SSHSession $SSH_Sessions['VC'].Value
                    }
                    catch {throw "ERROR: Unable to execute nc command on vCenter."}

                    if($SSHRes.ExitStatus -eq 1) {
                        Write-Error "* vCenter-to-LDAP TCP test FAILED."

                        # Netcat failed to access the TCP port.
                        # Let's have vCenter ping the LDAP server (even though ICMP isn't required)"
                        try {
                            $Command = "ping -c 3 $ldap_hostname"
                            $SSHRes = Invoke-SSHCommand -Command $Command -SSHSession $SSH_Sessions['VC'].Value
                        }
                        catch {throw "ERROR: Unable to execute ping command on vCenter."}
                        Write-Host "* vCenter-to-LDAP Ping test returned:"
                        Write-Host "$($SSHRes.Output | out-string)"

                        if($($SSHRes.Output | Out-String) -match " (?<percentage>[0-9]+)% packet loss") {
                            $ping_loss = $Matches.percentage
                            if($ping_loss -eq "0") {
                                Write-Host "* vCenter was able to ping the LDAP server without a problem."
                            } elseif($ping_loss -eq "100") {
                                Write-Error "* vCenter was unable to ping LDAP server."

                                # Okay, this is bad. vCenter can't contact the LDAP server with TCP
                                # nor ping, so let's do a traceroute for the network folks to look at:
                                try {
                                    $Command = "traceroute $ldap_hostname"
                                    $SSHRes = Invoke-SSHCommand -Command $Command -SSHSession $SSH_Sessions['VC'].Value
                                }
                                catch {throw "ERROR: Unable to execute traceroute command on vCenter."}
                                Write-Host "* vCenter-to-LDAP Traceroute test returned:"
                                Write-Host "$($SSHRes.Output | out-string)"
                            } else {
                                Write-Warning "* Partial packet loss pinging LDAP server."
                            }
                        } else {
                            Write-Error "* Unable to interpret ping results."
                        }
                    } elseif($SSHRes.ExitStatus -eq 0) {
                        Write-Host "* vCenter-to-LDAP TCP test successful. Port is open."

                        if($ldap_protocol -eq "ldaps") {
                            Write-Host "* Attempting SSL Connect test."
                            # Netcat says the TCP port is open, so let's attempt an SSL connection:
                            try {
                                $Command = "openssl s_client -connect $($ldap_hostname):$($ldap_port) -showcerts < /dev/null"
                                $SSHRes = Invoke-SSHCommand -Command $Command -SSHSession $SSH_Sessions['VC'].Value
                            }
                            catch {throw "ERROR: Unable to execute openssl command on vCenter."}
                            Write-Host "* SSL connect test returned:"
                            Write-Host "$($SSHRes.Output | out-string)"
                        }
                    } else {
                        Write-Error "* vCenter-to-LDAP TCP test failed with result code $($SSHRes.ExitStatus)."
                    }
                } else {
                    Write-Error "URL $url does not look like an LDAP URL."
                }
            }
        }
    }
    Write-Host "* Script terminated at $((Get-Date).ToUniversalTime())"
}

<#
    .Synopsis
     Recommended: Add a secure external identity source (Active Directory over LDAPS) for use with vCenter Server Single Sign-On.

    .Parameter Name
     The user-friendly name the external AD will be given in vCenter

    .Parameter DomainName
     Domain name of the external active directory, e.g. myactivedirectory.local

    .Parameter DomainAlias
     Domain alias of the external active directory, e.g. myactivedirectory

    .Parameter PrimaryUrl
     Url of the primary ldaps server to attempt to connect to, e.g. ldaps://myadserver.local:636

    .Parameter SecondaryUrl
     Optional: Url of the fallback ldaps server to attempt to connect to, e.g. ldaps://myadserver.local:636

    .Parameter BaseDNUsers
     Base Distinguished Name for users, e.g. "dc=myadserver,dc=local"

    .Parameter BaseDNGroups
     Base Distinguished Name for groups, e.g. "dc=myadserver,dc=local"

    .Parameter Credential
     Credential to login to the LDAP server (NOT cloudadmin) in the form of a username/password credential. Usernames often look like prodAdmins@domainname.com or if the AD is a Microsoft Active Directory server, usernames may need to be prefixed with the NetBIOS domain name, such as prod\AD_Admin

    .Parameter SSLCertificatesSasUrl
     An comma-delimeted list of Blob Shared Access Signature strings to the certificates required to connect to the external active directory

    .Parameter GroupName
     Optional: A group in the customer external identity source to be added to CloudAdmins. Users in this group will have CloudAdmin access. Group name should be formatted without the domain name, e.g. group-to-give-access

    .Example
    # Add the domain server named "myserver.local" to vCenter
    Add-LDAPSIdentitySource -Name 'myserver' -DomainName 'myserver.local' -DomainAlias 'myserver' -PrimaryUrl 'ldaps://10.40.0.5:636' -BaseDNUsers 'dc=myserver, dc=local' -BaseDNGroups 'dc=myserver, dc=local' -Username 'myserver@myserver.local' -Password 'PlaceholderPassword' -CertificatesSAS 'https://sharedaccessstring.path/accesskey' -Protocol LDAPS
#>

function New-LDAPSIdentitySource {
    [CmdletBinding(PositionalBinding = $false)]
    [AVSAttribute(10, UpdatesSDDC = $false)]
    Param
    (
        [Parameter(
            Mandatory = $true,
            HelpMessage = 'User-Friendly name to store in vCenter')]
        [ValidateNotNull()]
        [string]
        $Name,

        [Parameter(
            Mandatory = $true,
            HelpMessage = 'Full DomainName: adserver.local')]
        [ValidateNotNull()]
        [string]
        $DomainName,

        [Parameter(
            Mandatory = $true,
            HelpMessage = 'DomainAlias: adserver')]
        [string]
        $DomainAlias,

        [Parameter(
            Mandatory = $true,
            HelpMessage = 'URL of your AD Server: ldaps://yourserver:636')]
        [ValidateNotNullOrEmpty()]
        [string]
        $PrimaryUrl,

        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Optional: URL of a backup server')]
        [string]
        $SecondaryUrl,

        [Parameter(
            Mandatory = $true,
            HelpMessage = 'BaseDNGroups, "DC=name, DC=name"')]
        [ValidateNotNull()]
        [string]
        $BaseDNUsers,

        [Parameter(
            Mandatory = $true,
            HelpMessage = 'BaseDNGroups, "DC=name, DC=name"')]
        [ValidateNotNull()]
        [string]
        $BaseDNGroups,

        [Parameter(Mandatory = $true,
            HelpMessage = "Credential for the LDAP server")]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential,

        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Optional: The certs will be installed from domain controllers if not specified. A comma-delimited list of SAS path URI to Certificates for authentication. Ensure permissions to read included. To generate, place the certificates in any storage account blob and then right click the cert and generate SAS')]
        [System.Security.SecureString]
        $SSLCertificatesSasUrl,

        [Parameter (
            Mandatory = $false,
            HelpMessage = 'A group in the external identity source to give CloudAdmins access')]
        [string]
        $GroupName
    )

    if (-not ($PrimaryUrl -match '^(ldaps:).+((:389)|(:636)|(:3268)|(:3269))$')) {
        Write-Error "PrimaryUrl $PrimaryUrl is invalid. Ensure the port number is 389, 636, 3268, or 3269 and that the url begins with ldaps: and not ldap:" -ErrorAction Stop
    }
    if (($PrimaryUrl -match '^(ldaps:).+((:389)|(:3268))$')) {
        Write-Warning "PrimaryUrl $PrimaryUrl is nonstandard. Are you sure you meant to use the 389/3268 port and not the standard ports for LDAPS, 636 or 3269? Continuing anyway.."
    }
    if ($PSBoundParameters.ContainsKey('SecondaryUrl') -and (-not ($SecondaryUrl -match '^(ldaps:).+((:389)|(:636)|(:3268)|(:3269))$'))) {
        Write-Error "SecondaryUrl $SecondaryUrl is invalid. Ensure the port number is 389, 636, 3268, or 3269 and that the url begins with ldaps: and not ldap:" -ErrorAction Stop
    }
    if (($SecondaryUrl -match '^(ldaps:).+((:389)|(:3268))$')) {
        Write-Warning "SecondaryUrl $SecondaryUrl is nonstandard. Are you sure you meant to use the 389/3268 port and not the standard ports for LDAPS, 636 or 3269? Continuing anyway.."
    }

    $ExternalIdentitySources = Get-IdentitySource -External -ErrorAction Continue
    if ($null -ne $ExternalIdentitySources) {
        Write-Host "Checking to see if identity source already exists..."
        if ($DomainName.trim() -eq $($ExternalIdentitySources.Name.trim())) {
            Write-Error $($ExternalIdentitySources | Format-List | Out-String) -ErrorAction Continue
            Write-Error "Already have an external identity source with the same name: $($ExternalIdentitySources.Name). If only trying to add a group to this Identity Source, use Add-GroupToCloudAdmins" -ErrorAction Stop
        }
        else {
            Write-Information "$($ExternalIdentitySources | Format-List | Out-String)"
            Write-Information "An identity source already exists, but not for this domain. Continuing to add this one..."
        }
    }
    else {
        Write-Host "No existing external identity sources found."
    }

    $Password = $Credential.GetNetworkCredential().Password
    $DestinationFileArray = @()
    $remoteComputers = , $PrimaryUrl
    if ($PSBoundParameters.ContainsKey('SecondaryUrl')) {
        $remoteComputers += $SecondaryUrl
    }

    # check the connection between domain servers and the vcenter
    foreach ($computerUrl in $remoteComputers) {
        if (![uri]::IsWellFormedUriString($computerUrl, 'Absolute')) {
            throw "Incorrect Url format entered from: $computerUrl"
        }
        $ParsedUrl = [System.Uri]$computerUrl
        if ($ParsedUrl.Port -lt 0 -OR $ParsedUrl.Host -eq "" -OR $ParsedUrl.Scheme -eq "") {
            throw "Incorrect Url format entered from: $computerUrl. The correct Url format is protocol://host:port (Example: ldaps://yourserver.com:636)."
        }
        $ResultUrlString = $ParsedUrl.GetLeftPart([UriPartial]::Authority)
        $ResultUrl = [System.Uri]$ResultUrlString
        if ($ResultUrl.Host -match "^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$" -and [bool]($ResultUrl.Host -as [ipaddress])) {
            throw "Incorrect Url format. $computerUrl is an IP address. Please use the hostname exactly as specified on the issued certificate."
        }
        # dns lookup
        try {
            $Command = 'nslookup ' + $ResultUrl.Host + ' -type=soa'
            $SSHRes = Invoke-SSHCommand -Command $Command -SSHSession $SSH_Sessions['VC'].Value
            if ($SSHRes.ExitStatus -ne 0) { throw "$SSHRes" }
            $IPAddress = $SSHRes.Output | Select-String "Address:" | Where-Object { $_ -notmatch "#" } | ForEach-Object { $_.ToString().Split()[1] } | Select-Object -First 1
            if (-Not ($IPAddress -as [ipaddress])) { throw "The FQDN $($ResultUrl.Host) failed to resolved to an IP address or incorrect IP format. Make sure DNS is configured correctly." }
        }
        catch {
            throw "The FQDN $($ResultUrl.Host) cannot be resolved to an IP address. Make sure DNS is configured. $_"
        }
        Write-Host "The FQDN $($ResultUrl.Host) is resolved successfully."
        # reverse dns lookup
        try {
            $Command = 'nslookup ' + $IPAddress
            $SSHRes = Invoke-SSHCommand -Command $Command -SSHSession $SSH_Sessions['VC'].Value
            if ($SSHRes.ExitStatus -ne 0) { throw "The FQDN $($ResultUrl.Host) is resolved successfully but the IP address $($IPAddress) does not have a corresponding DNS PTR (pointer) record, which is used for reverse DNS lookups. Make sure DNS is configured. $SSHRes" }
        }
        catch {
            throw "The FQDN $($ResultUrl.Host) failed to do a reverse DNS lookup. $_"
        }
        # check whether a specific port (or range of ports) on a target ip address is open or closed
        try {
            $Command = 'nc -nvz ' + $IPAddress + ' ' + $ResultUrl.Port
            $SSHRes = Invoke-SSHCommand -Command $Command -SSHSession $SSH_Sessions['VC'].Value
            if ($SSHRes.ExitStatus -ne 0) { throw "$SSHRes" }
        }
        catch {
            throw "The connection cannot be established. Please check the address, routing and/or firewall and make sure port $($ResultUrl.Port) is open. $_"
        }
        Write-Host "Connectivity to $($ResultUrl.Host):$($ResultUrl.Port) is verified."
    }

    if ($PSBoundParameters.ContainsKey('SSLCertificatesSasUrl')) {
        $DestinationFileArray = Get-Certificates -SSLCertificatesSasUrl $SSLCertificatesSasUrl -ErrorAction Stop
    }
    else {
        $DestinationFileArray = Get-CertificateFromServerToLocalFile $remoteComputers
    }

    [System.Array]$Certificates =
    foreach ($certFile in $DestinationFileArray) {
        try {
            New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($certfile)
        }
        catch {
            Write-Error "Failure to convert file $certfile to a certificate $($PSItem.Exception.Message)"
            throw "File to certificate conversion failed. See error message for more details"
        }
    }
    # check if the certicates expire or not
    foreach ($cert in $Certificates) {
        $currentDate = Get-Date
        Write-Host "Verifying certificate: $($cert.Subject)"
        if (($cert.NotBefore -lt $currentDate) -and ($cert.NotAfter -gt $currentDate)) {
            Write-Host "The certificate is current."
        } else {
            Write-Error "The certificate is not current. It's only valid between $($cert.NotBefore) and $($cert.NotAfter)." -ErrorAction Stop
        }
    }

    Write-Host "Adding the LDAPS Identity Source..."
    try {
        Add-LDAPIdentitySource `
        -Name $Name `
        -DomainName $DomainName `
        -DomainAlias $DomainAlias `
        -PrimaryUrl $PrimaryUrl `
        -SecondaryUrl $SecondaryUrl `
        -BaseDNUsers $BaseDNUsers `
        -BaseDNGroups $BaseDNGroups `
        -Username $Credential.UserName `
        -Password $Password `
        -ServerType 'ActiveDirectory' `
        -Certificates $Certificates -ErrorAction Stop
    }
    catch {
        Write-Error "VCenter wasn't able to add this identity source: $_" -ErrorAction Stop
    }
    $ExternalIdentitySources = Get-IdentitySource -External -ErrorAction Continue
    $ExternalIdentitySources | Format-List | Out-String

    if ($PSBoundParameters.ContainsKey('GroupName')) {
        Write-Host "GroupName passed in: $GroupName"
        Write-Host "Attempting to add group $GroupName to CloudAdmins..."
        Add-GroupToCloudAdmins -GroupName $GroupName -Domain $DomainName -ErrorAction Stop
    }
}

<#
    .Synopsis
     Update the SSL Certificates used for authenticating to an Active Directory over LDAPS

    .Parameter DomainName
     Domain name of the external active directory, e.g. myactivedirectory.local

    .Parameter SSLCertificatesSasUrl
     A comma-delimeted string of the shared access signature (SAS) URLs linking to the certificates required to connect to the external active directory. If more than one, separate each SAS URL by a comma `,`.
#>

function Update-IdentitySourceCertificates {
    [CmdletBinding(PositionalBinding = $false)]
    [AVSAttribute(10, UpdatesSDDC = $false)]
    Param
    (
        [Parameter(
            Mandatory = $true,
            HelpMessage = 'Name of the Identity source')]
        [ValidateNotNull()]
        [string]
        $DomainName,

        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Optional: The certs will be downloaded from domain controllers if not specified. A comma-delimited list of SAS path URI to Certificates for authentication. Ensure permissions to read included. To generate, place the certificates in any storage account blob and then right click the cert and generate SAS')]
        [System.Security.SecureString]
        $SSLCertificatesSasUrl
    )

    $ExternalIdentitySources = Get-IdentitySource -External -ErrorAction Stop
    if ($null -ne $ExternalIdentitySources) {
        $IdentitySource = $ExternalIdentitySources | Where-Object { $_.Name -eq $DomainName }
        if ($null -ne $IdentitySource) {
            if ($PSBoundParameters.ContainsKey('SSLCertificatesSasUrl')) {
                $DestinationFileArray = Get-Certificates $SSLCertificatesSasUrl -ErrorAction Stop
            }
            else {
                $remoteComputers = @()
                if ($null -ne $IdentitySource.PrimaryUrl) {
                    $remoteComputers += $IdentitySource.PrimaryUrl
                    Write-Host "* The Primary URL is $($IdentitySource.PrimaryUrl)."
                }
                else {
                    Write-Error "Internal Error: The primary url of identity source is null." -ErrorAction Stop
                }

                if ($null -ne $IdentitySource.FailoverUrl) {
                    $remoteComputers += $IdentitySource.FailoverUrl
                    Write-Host "* The Failover URL is $($IdentitySource.FailoverUrl)."
                }
                $DestinationFileArray = Get-CertificateFromServerToLocalFile $remoteComputers
            }
            [System.Array]$Certificates =
            foreach ($CertFile in $DestinationFileArray) {
                try {
                    [System.Security.Cryptography.X509Certificates.X509Certificate2]::CreateFromCertFile($certfile)
                }
                catch {
                    Write-Error "Failure to convert file $certfile to a certificate $($PSItem.Exception.Message)"
                    throw "File to certificate conversion failed. See error message for more details"
                }
            }
            Write-Host "Updating the LDAPS Identity Source..."
            Set-LDAPIdentitySource -IdentitySource $IdentitySource -Certificates $Certificates -ErrorAction Stop
            $ExternalIdentitySources = Get-IdentitySource -External -ErrorAction Continue
            $ExternalIdentitySources | Format-List | Out-String
        }
        else {
            Write-Error "Could not find Identity Source with name: $DomainName." -ErrorAction Stop
        }
    }
    else {
        Write-Host "No existing external identity sources found."
    }
}

<#
    .Synopsis
     Update the password used in the credential to authenticate an LDAP server
    .Parameter Credential
     Credential to login to the LDAP server (NOT cloudadmin) in the form of a username/password credential. Usernames often look like prodAdmins@domainname.com or if the AD is a Microsoft Active Directory server, usernames may need to be prefixed with the NetBIOS domain name, such as prod\AD_Admin

     .Parameter DomainName
     Domain name of the external LDAP server, e.g. myactivedirectory.local
#>

function Update-IdentitySourceCredential {
    [CmdletBinding(PositionalBinding = $false)]
    [AVSAttribute(10, UpdatesSDDC = $false)]
    Param
    (
        [Parameter(
            Mandatory = $true,
            HelpMessage = 'Name of the Identity source')]
        [ValidateNotNull()]
        [string]
        $DomainName,

        [Parameter(Mandatory = $true,
            HelpMessage = "Credential for the LDAP server")]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential
    )

    $ExternalIdentitySources = Get-IdentitySource -External -ErrorAction Stop
    if ($null -ne $ExternalIdentitySources) {
        $IdentitySource = $ExternalIdentitySources | Where-Object { $_.Name -eq $DomainName }
        if ($null -ne $IdentitySource) {
            Write-Host "Updating the LDAP Identity Source..."
            Set-LDAPIdentitySource -IdentitySource $IdentitySource -Credential $Credential -ErrorAction Stop
            $ExternalIdentitySources = Get-IdentitySource -External -ErrorAction Continue
            $ExternalIdentitySources | Format-List | Out-String
        }
        else {
            throw "Could not find Identity Source with name: $DomainName."
        }
    }
    else {
        throw "No existing external identity sources found."
    }
}

<#
    .Synopsis
     Gets all external identity sources
#>

function Get-ExternalIdentitySources {
    [AVSAttribute(3, UpdatesSDDC = $false)]
    Param()

    $ExternalSource = Get-IdentitySource -External
    if ($null -eq $ExternalSource) {
        Write-Output "No external identity sources found."
        return
    }
    else {
        Write-Output "LDAPs Certificate(s) valid until the [Not After] parameter"
        $ExternalSource | Format-List | Out-String
    }
}

<#
    .Synopsis
     Removes supplied identity source, or, if no specific identity source is provided, will remove all identity sources.

    .Parameter DomainName
     The domain name of the external identity source to remove i.e. `mydomain.com`. If none provided, will attempt to remove all external identity sources.
#>

function Remove-ExternalIdentitySources {
    [AVSAttribute(5, UpdatesSDDC = $false)]
    Param
    (
        [Parameter(Mandatory = $false)]
        [string]
        $DomainName
    )

    $ExternalSource = Get-IdentitySource -External
    if ($null -eq $ExternalSource) {
        Write-Output "No external identity sources found to remove. Nothing done"
        return
    }
    else {
        if (-Not ($PSBoundParameters.ContainsKey('DomainName'))) {
            foreach ($AD in $ExternalSource) {
                Remove-IdentitySource -IdentitySource $AD -ErrorAction Stop
                Write-Output "Identity source $($AD.Name) removed."
            }
        }
        else {
            $FoundMatch = $false
            foreach ($AD in $ExternalSource) {
                if ($AD.Name -eq $DomainName) {
                    Remove-IdentitySource -IdentitySource $AD -ErrorAction Stop
                    Write-Output "Identity source $($AD.Name) removed."
                    $FoundMatch = $true
                }
            }
            if (-Not $FoundMatch) { Write-Output "No external identity source found that matches $DomainName. Nothing done." }
        }
    }
}

<#
    .Synopsis
     Add a group from the external identity to the CloudAdmins group

    .Parameter GroupName
     The group in the customer external identity source to be added to CloudAdmins. Users in this group will have CloudAdmin access. Group name should be formatted without the domain name, e.g. group-to-give-access

    .Parameter Domain
     Name of the external domain that GroupName is in. If not provided, will attempt to locate the group in all the configured active directories. For example, MyActiveDirectory.Com

    .Example
    # Add the group named vsphere-admins to CloudAdmins
     Add-GroupToCloudAdmins -GroupName 'vsphere-admins'
#>

function Add-GroupToCloudAdmins {
    [CmdletBinding(PositionalBinding = $false)]
    [AVSAttribute(10, UpdatesSDDC = $false)]
    Param
    (
        [Parameter(
            Mandatory = $true,
            HelpMessage = 'Name of the group to add to CloudAdmin')]
        [ValidateNotNull()]
        [string]
        $GroupName,

        [Parameter(Mandatory = $false)]
        [string]
        $Domain
    )

    $ExternalSources
    $GroupToAdd
    $Domain

    try {
        $ExternalSources = Get-IdentitySource -External -ErrorAction Stop
    }
    catch {
        Write-Error $PSItem.Exception.Message -ErrorAction Continue
        Write-Error "Unable to get external identity source" -ErrorAction Stop
    }

    # Searching the external identities for the domain
    if ($null -eq $ExternalSources -or 0 -eq $ExternalSources.count) {
        Write-Error "No external identity source found. Please run New-LDAPSIdentitySource first" -ErrorAction Stop
    }
    elseif ($ExternalSources.count -eq 1) {
        if ($PSBoundParameters.ContainsKey('Domain')) {
            if ($Domain -ne $ExternalSources.Name) {
                Write-Error "The Domain passed in ($Domain) does not match the external directory: $($ExternalSources.Name). Try again with -Domain $($ExternalSources.Name)" -ErrorAction Stop
            }
        }
    }
    elseif ($ExternalSources.count -gt 1) {
        if (-Not ($PSBoundParameters.ContainsKey('Domain'))) {
            Write-Host "Multiple external identites exist and domain not suplied. Will attempt to search all ADs attached for $GroupName"
        }
        else {
            $FoundDomainMatch = $false
            foreach ($AD in $ExternalSources) {
                if ($AD.Name -eq $Domain) {
                    $FoundDomainMatch = $true
                    break
                }
            }
            if (-Not $FoundDomainMatch) {
                Write-Warning "Searched the External Directories: $($ExternalSources | Format-List | Out-String) for $Domain and did not find a match"
                Write-Error "Was not able to find $Domain in any of the External Directories" -ErrorAction Stop
            }
        }
    }

    # Searching for the group in the specified domain, if provided, or all domains, if none provided
    if ($null -eq $Domain -or -Not ($PSBoundParameters.ContainsKey('Domain'))) {
        $FoundMatch = $false
        foreach ($AD in $ExternalSources) {
            Write-Host "Searching $($AD.Name) for $GroupName"
            try {
                $GroupFound = Get-SsoGroup -Name $GroupName -Domain $AD.Name -ErrorAction Stop
            }
            catch {
                Write-Host "Could not find $GroupName in $($AD.Name). Continuing.."
            }
            if ($null -ne $GroupFound -and -Not $FoundMatch) {
                Write-Host "Found $GroupName in $($AD.Name)."
                $Domain = $AD.Name
                $GroupToAdd = $GroupFound
                $FoundMatch = $true
            }
            elseif ($null -ne $GroupFound -and $FoundMatch) {
                Write-Host "Found $GroupName in $($AD.Name) as well."
                Write-Error "Group $GroupName exists in multiple domains . Please re-run and specify domain" -ErrorAction Stop
                return
            }
            elseif ($null -eq $GroupFound) {
                Write-Host "$GroupName not found in $($AD.Name)"
            }
        }
        if ($null -eq $GroupToAdd) {
            Write-Error "$GroupName was not found in any external identity that has been configured. Please ensure that the group name is typed correctly." -ErrorAction Stop
        }
    }
    else {
        try {
            Write-Host "Searching $Domain for $GroupName..."
            $GroupToAdd = Get-SsoGroup -Name $GroupName -Domain $Domain -ErrorAction Stop
        }
        catch {
            Write-Error "Exception $($PSItem.Exception.Message): Unable to get group $GroupName from $Domain" -ErrorAction Stop
        }
    }

    if ($null -eq $GroupToAdd) {
        Write-Error "$GroupName was not found in the domain. Please ensure that the group is spelled correctly" -ErrorAction Stop
    }
    else {
        Write-Host "Adding $GroupToAdd to CloudAdmins...."
    }

    $CloudAdmins = Get-SsoGroup -Name 'CloudAdmins' -Domain 'vsphere.local'
    if ($null -eq $CloudAdmins) {
        Write-Error "Internal Error fetching CloudAdmins group. Contact support" -ErrorAction Stop
    }

    $GroupToAddTuple = [System.Tuple]::Create("$($GroupToAdd.Name)", "$($GroupToAdd.Domain)")
    $CloudAdminMembers = @()
    foreach ($a in $(Get-SsoGroup -Group $CloudAdmins)) { $tuple = [System.Tuple]::Create("$($a.Name)", "$($a.Domain)"); $CloudAdminMembers += $tuple }
    if ($GroupToAddTuple -in $CloudAdminMembers) {
        Write-Host "Group $($GroupToAddTuple.Item1)@$($($GroupToAddTuple.Item2)) has already been added to CloudAdmins."
        return
    }

    try {
        Write-Host "Adding group $GroupName to CloudAdmins..."
        Add-GroupToSsoGroup -Group $GroupToAdd -TargetGroup $CloudAdmins -ErrorAction Stop
    }
    catch {
        $CloudAdminMembers = Get-SsoGroup -Group $CloudAdmins -ErrorAction Continue
        Write-Warning "Cloud Admin Members: $CloudAdminMembers" -ErrorAction Continue
        Write-Error "Unable to add group to CloudAdmins. Error: $($PSItem.Exception.Message)" -ErrorAction Stop
    }

    Write-Host "Successfully added $GroupName to CloudAdmins."
    $CloudAdminMembers = Get-SsoGroup -Group $CloudAdmins -ErrorAction Continue
    Write-Output "Cloud Admin Members: $CloudAdminMembers"
}

<#
    .Synopsis
     Remove a previously added group from an external identity from the CloudAdmins group

    .Parameter GroupName
     The group in the customer external identity source to be removed from CloudAdmins. Group name should be formatted without the domain name, e.g. group-to-give-access

    .Parameter Domain
     Name of the external domain that GroupName is in. If not provided, will attempt to locate the group in all the configured active directories. For example, MyActiveDirectory.Com

    .Example
    # Remove the group named vsphere-admins from CloudAdmins
     Remove-GroupFromCloudAdmins -GroupName 'vsphere-admins'
#>

function Remove-GroupFromCloudAdmins {
    [CmdletBinding(PositionalBinding = $false)]
    [AVSAttribute(10, UpdatesSDDC = $false)]
    Param
    (
        [Parameter(
            Mandatory = $true,
            HelpMessage = 'Name of the group to remove from CloudAdmin')]
        [ValidateNotNull()]
        [string]
        $GroupName,

        [Parameter(Mandatory = $false)]
        [string]
        $Domain
    )

    $ExternalSources
    $GroupToRemove
    $Domain

    try {
        $ExternalSources = Get-IdentitySource -External -ErrorAction Stop
    }
    catch {
        Write-Error $PSItem.Exception.Message -ErrorAction Continue
        Write-Error "Unable to get external identity source" -ErrorAction Stop
    }

    # Searching the external identities for the domain
    if ($null -eq $ExternalSources -or 0 -eq $ExternalSources.count) {
        Write-Error "No external identity source found. Please run New-LDAPSIdentitySource first" -ErrorAction Stop
    }
    elseif ($ExternalSources.count -eq 1) {
        if ($PSBoundParameters.ContainsKey('Domain')) {
            if ($Domain -ne $ExternalSources.Name) {
                Write-Error "The Domain passed in ($Domain) does not match the external directory: $($ExternalSources.Name)" -ErrorAction Stop
            }
        }
    }
    elseif ($ExternalSources.count -gt 1) {
        if (-Not ($PSBoundParameters.ContainsKey('Domain'))) {
            Write-Host "Multiple external identites exist and domain not suplied. Will attempt to search all ADs attached for $GroupName"
        }
        else {
            $FoundDomainMatch = $false
            foreach ($AD in $ExternalSources) {
                if ($AD.Name -eq $Domain) {
                    $FoundDomainMatch = $true
                    break
                }
            }
            if (-Not $FoundDomainMatch) {
                Write-Warning "Searched the External Directories: $($ExternalSources | Format-List | Out-String) for $Domain and did not find a match"
                Write-Error "Was not able to find $Domain in any of the External Directories" -ErrorAction Stop
            }
        }
    }

    # Searching for the group in the specified domain, if provided, or all domains, if none provided
    if ($null -eq $Domain -or -Not ($PSBoundParameters.ContainsKey('Domain'))) {
        $FoundMatch = $false
        foreach ($AD in $ExternalSources) {
            Write-Host "Searching $($AD.Name) for $GroupName"
            try {
                $GroupFound = Get-SsoGroup -Name $GroupName -Domain $AD.Name -ErrorAction Stop
            }
            catch {
                Write-Host "Could not find $GroupName in $($AD.Name). Continuing.."
            }
            if ($null -ne $GroupFound -and -Not $FoundMatch) {
                Write-Host "Found $GroupName in $($AD.Name)."
                $Domain = $AD.Name
                $GroupToRemove = $GroupFound
                $FoundMatch = $true
            }
            elseif ($null -ne $GroupFound -and $FoundMatch) {
                Write-Host "Found $GroupName in $($AD.Name) as well."
                Write-Error "Group $GroupName exists in multiple domains . Please re-run and specify domain" -ErrorAction Stop
                return
            }
            elseif ($null -eq $GroupFound) {
                Write-Host "$GroupName not found in $($AD.Name)"
            }
        }
        if ($null -eq $GroupToRemove) {
            Write-Error "$GroupName was not found in any external identity that has been configured. Please ensure that the group name is typed correctly." -ErrorAction Stop
        }
    }
    else {
        try {
            Write-Host "Searching $Domain for $GroupName..."
            $GroupToRemove = Get-SsoGroup -Name $GroupName -Domain $Domain -ErrorAction Stop
        }
        catch {
            Write-Error "Exception $($PSItem.Exception.Message): Unable to get group $GroupName from $Domain" -ErrorAction Stop
        }
    }

    if ($null -eq $GroupToRemove) {
        Write-Error "$GroupName was not found in $Domain. Please ensure that the group is spelled correctly" -ErrorAction Stop
    }
    else {
        Write-Host "Removing $GroupToRemove from CloudAdmins...."
    }

    $CloudAdmins = Get-SsoGroup -Name 'CloudAdmins' -Domain 'vsphere.local'
    if ($null -eq $CloudAdmins) {
        Write-Error "Internal Error fetching CloudAdmins group. Contact support" -ErrorAction Stop
    }

    try {
        Remove-GroupFromSsoGroup -Group $GroupToRemove -TargetGroup $CloudAdmins -ErrorAction Stop
    }
    catch {
        $CloudAdminMembers = Get-SsoGroup -Group $CloudAdmins -ErrorAction Continue
        Write-Error "Current Cloud Admin Members: $CloudAdminMembers" -ErrorAction Continue
        Write-Error "Unable to remove group from CloudAdmins. Is it there at all? Error: $($PSItem.Exception.Message)" -ErrorAction Stop
    }

    Write-Information "Group $GroupName successfully removed from CloudAdmins."
    $CloudAdminMembers = Get-SsoGroup -Group $CloudAdmins -ErrorAction Continue
    Write-Output "Current Cloud Admin Members: $CloudAdminMembers"
}

<#
    .Synopsis
     Get all groups that have been added to the cloud admin group
    .Example
    # Get all users in CloudAdmins
     Get-CloudAdminGroups
#>

function Get-CloudAdminGroups {
    [CmdletBinding(PositionalBinding = $false)]
    [AVSAttribute(3, UpdatesSDDC = $false)]
    Param()

    $CloudAdmins = Get-SsoGroup -Name 'CloudAdmins' -Domain 'vsphere.local'
    if ($null -eq $CloudAdmins) {
        Write-Error "Internal Error fetching CloudAdmins group. Contact support" -ErrorAction Stop
    }

    $CloudAdminMembers = Get-SsoGroup -Group $CloudAdmins -ErrorAction Stop
    if ($null -eq $CloudAdminMembers) {
        Write-Output "No groups yet added to CloudAdmin."
    }
    else {
        $CloudAdminMembers | Format-List | Out-String
    }
}

<#
    .Synopsis
     Gets all the vSAN based storage policies available to set on a VM.
#>

function Get-StoragePolicies {
    [AVSAttribute(3, UpdatesSDDC = $False)]
    Param()

    $StoragePolicies
    try {
        $StoragePolicies = Get-SpbmStoragePolicy -Namespace "VSAN" -ErrorAction Stop | Select-Object Name, AnyOfRuleSets
    }
    catch {
        Write-Error $PSItem.Exception.Message -ErrorAction Continue
        Write-Error "Unable to get storage policies" -ErrorAction Stop
    }
    if ($null -eq $StoragePolicies) {
        Write-Host "Could not find any storage policies."
    }
    else {
        Write-Output "Available Storage Policies:"
        $StoragePolicies | Format-List | Out-String
    }
}

<#
    .Synopsis
     Modify vSAN based storage policies on a VM(s)

    .Parameter StoragePolicyName
     Name of a vSAN based storage policy to set on the specified VM. Options can be seen in vCenter or using the Get-StoragePolicies command.

    .Parameter VMName
     Name of the VM to set the vSAN based storage policy on. This supports wildcards for bulk operations. For example, MyVM* would attempt to change the storage policy on MyVM1, MyVM2, MyVM3, etc.

    .Example
    # Set the vSAN based storage policy on MyVM to RAID-1 FTT-1
    Set-VMStoragePolicy -StoragePolicyName "RAID-1 FTT-1" -VMName "MyVM"
#>

function Set-VMStoragePolicy {
    [CmdletBinding(PositionalBinding = $false)]
    [AVSAttribute(10, UpdatesSDDC = $True)]
    Param
    (
        [Parameter(
            Mandatory = $true,
            HelpMessage = 'Name of the storage policy to set')]
        [ValidateNotNullOrEmpty()]
        [string]
        $StoragePolicyName,

        [Parameter(
            Mandatory = $true,
            HelpMessage = 'Name of the VM to set the storage policy on')]
        [ValidateNotNullOrEmpty()]
        [string]
        $VMName
    )
    $StoragePolicy, $VSANStoragePolicies = Get-StoragePolicyInternal $StoragePolicyName -ErrorAction Stop
    $VMList = Get-VM $VMName

    if ($null -eq $VMList) {
        Write-Error "Was not able to set the storage policy on the VM. Could not find VM(s) with the name: $VMName" -ErrorAction Stop
    }
    elseif ($VMList.count -eq 1) {
        $VM = $VMList[0]
        Set-StoragePolicyOnVM -VM $VM -VSANStoragePolicies $VSANStoragePolicies -StoragePolicy $StoragePolicy -ErrorAction Stop
    }
    else {
        foreach ($VM in $VMList) {
            Set-StoragePolicyOnVM -VM $VM -VSANStoragePolicies $VSANStoragePolicies -StoragePolicy $StoragePolicy -ErrorAction Continue
        }
    }
}

<#
    .Synopsis
     Modify vSAN based storage policies on all VMs in a Container

    .Parameter StoragePolicyName
     Name of a vSAN based storage policy to set on the specified VM. Options can be seen in vCenter or using the Get-StoragePolicies command.

    .Parameter Location
     Name of the Folder, ResourcePool, or Cluster containing the VMs to set the storage policy on.
     For example, if you would like to change the storage policy of all the VMs in the cluster "Cluster-2", then supply "Cluster-2".
     Similarly, if you would like to change the storage policy of all the VMs in a folder called "MyFolder", supply "MyFolder"

    .Example
    # Set the vSAN based storage policy on all VMs in MyVMs to RAID-1 FTT-1
    Set-LocationStoragePolicy -StoragePolicyName "RAID-1 FTT-1" -Location "MyVMs"
#>

function Set-LocationStoragePolicy {
    [CmdletBinding(PositionalBinding = $false)]
    [AVSAttribute(10, UpdatesSDDC = $True)]
    Param
    (
        [Parameter(
            Mandatory = $true,
            HelpMessage = 'Name of the storage policy to set')]
        [ValidateNotNullOrEmpty()]
        [string]
        $StoragePolicyName,

        [Parameter(
            Mandatory = $true,
            HelpMessage = 'Name of the Folder, ResourcePool, or Cluster containing the VMs to set the storage policy on.')]
        [ValidateNotNullOrEmpty()]
        [string]
        $Location
    )
    $StoragePolicy, $VSANStoragePolicies = Get-StoragePolicyInternal $StoragePolicyName -ErrorAction Stop
    $VMList = Get-VM -Location $Location

    if ($null -eq $VMList) {
        Write-Error "Was not able to set storage policies. Could not find VM(s) in the container: $Location" -ErrorAction Stop
    }
    else {
        foreach ($VM in $VMList) {
            Set-StoragePolicyOnVM -VM $VM -VSANStoragePolicies $VSANStoragePolicies -StoragePolicy $StoragePolicy -ErrorAction Continue
        }
    }
}

<#
    .Synopsis
     Specify default storage policy for a cluster(s)

    .Parameter StoragePolicyName
     Name of a vSAN based storage policy to set to be the default for VMs on this cluster. Options can be seen in vCenter or using the Get-StoragePolicies command.

    .Parameter ClusterName
     Name of the cluster to set the default on. This supports wildcards for bulk operations. For example, MyCluster* would attempt to change the storage policy on MyCluster1, MyCluster2, etc.

    .Example
    # Set the default vSAN based storage policy on MyCluster to RAID-1 FTT-1
    Set-ClusterDefaultStoragePolicy -StoragePolicyName "RAID-1 FTT-1" -ClusterName "MyCluster"
#>

function Set-ClusterDefaultStoragePolicy {
    [CmdletBinding(PositionalBinding = $false)]
    [AVSAttribute(10, UpdatesSDDC = $True)]
    Param
    (
        [Parameter(
            Mandatory = $true,
            HelpMessage = 'Name of the storage policy to set')]
        [ValidateNotNullOrEmpty()]
        [string]
        $StoragePolicyName,

        [Parameter(
            Mandatory = $true,
            HelpMessage = 'Name of the Cluster to set the storage policy on')]
        [ValidateNotNullOrEmpty()]
        [string]
        $ClusterName
    )
    $StoragePolicy, $VSANStoragePolicies = Get-StoragePolicyInternal $StoragePolicyName
    $CompatibleDatastores = Get-SpbmCompatibleStorage -StoragePolicy $StoragePolicy
    $ClusterList = Get-Cluster $ClusterName
    if ($null -eq $ClusterList) {
        Write-Error "Could not find Cluster with the name $ClusterName." -ErrorAction Stop
    }

    $ClusterDatastores = $ClusterList | Get-VMHost | Get-Datastore

    if ($null -eq $ClusterDatastores) {
        $hosts = $ClusterList | Get-VMHost
        if ($null -eq $hosts) {
            Write-Error "Was not able to set the Storage policy on $ClusterList. The Cluster does not appear to have VM Hosts. Please add VM Hosts before setting storage policy" -ErrorAction Stop
        }
        else {
            Write-Error "Setting the Storage Policy on this Cluster is not supported." -ErrorAction Stop
        }
    }
    elseif ($ClusterDatastores.count -eq 1) {
        if ($ClusterDatastores[0] -in $CompatibleDatastores) {
            try {
                Write-Host "Setting Storage Policy on $ClusterList to $StoragePolicyName..."
                Set-SpbmEntityConfiguration -Configuration (Get-SpbmEntityConfiguration $ClusterDatastores[0]) -storagePolicy $StoragePolicy -ErrorAction Stop -Confirm:$false
                Write-Output "Successfully set the Storage Policy on $ClusterList to $StoragePolicyName"
            }
            catch {
                Write-Error "Was not able to set the Storage Policy on the Cluster Datastore: $($PSItem.Exception.Message)" -ErrorAction Stop
            }
        }
        else {
            Write-Error "Modifying the default storage policy on this cluster: $($ClusterDatastores[0]) is not supported" -ErrorAction Stop
        }
    }
    else {
        foreach ($Datastore in $ClusterDatastores) {
            if ($Datastore -in $CompatibleDatastores) {
                try {
                    Write-Host "Setting Storage Policy on $Datastore to $StoragePolicyName..."
                    Set-SpbmEntityConfiguration -Configuration (Get-SpbmEntityConfiguration $Datastore) -storagePolicy $StoragePolicy -ErrorAction Stop -Confirm:$false
                    Write-Output "Successfully set the storage policy on $Datastore to $StoragePolicyName"
                }
                catch {
                    Write-Error "Was not able to set the storage policy on the Cluster Datastore: $($PSItem.Exception.Message)" -ErrorAction Stop
                }
            }
            else {
                Write-Error "Modifying the default storage policy on $Datastore is not supported" -ErrorAction Continue
                continue
            }
        }
    }
}

<#
    .Synopsis
     This will create a folder on every datastore (/vmfs/volumes/datastore/tools-repo) and set the ESXi hosts to use that folder as the tools-repo.
     The customer is responsible for putting the VMware Tools zip file in a publicly available HTTP(S) downloadable location.

     .EXAMPLE
     Once the function is imported, you simply need to run Set-ToolsRepo -ToolsURL <url to tools zip file>
#>

function Set-ToolsRepo {
    [AVSAttribute(30, UpdatesSDDC = $false)]
    param(
        [Parameter(Mandatory = $true,
            HelpMessage = "A publiclly available HTTP(S) URL to download the Tools zip file.")]
        [SecureString]
        $ToolsURL
    )

    # Tools repo folder
    $newFolder = 'tools-repo'

    # Get all datastores
    $datastores = Get-Datastore -ErrorAction Stop | Where-Object { $_.extensionData.Summary.Type -eq "vsan" }

    $tools_url = ConvertFrom-SecureString $ToolsURL -AsPlainText
    # Download the new tools files
    Invoke-WebRequest -Uri $tools_url -OutFile "newtools.zip"
    Expand-Archive "./newtools.zip" -ErrorAction Stop

    # Make sure the new tools files exist
    If (!(Test-Path "./newtools/vmtools")) {
        Write-Error -Message "Unable to find new tools files"
        throw "Unable to find new tools files"
    }

    foreach ($datastore in $datastores) {
        # Get datastore name
        $ds_name = $datastore.Name

        # Get ID of the vsanDatastore requested
        $ds_id = Get-Datastore -Name $ds_name | Select-Object -Property Id

        # Create the PS drive
        New-PSDrive -Location $datastore -Name DS -PSProvider VimDatastore -Root "\" | Out-Null

        # Does repo folder exist?
        $Dsbrowser = Get-View -Id $Datastore.Extensiondata.Browser
        $spec = New-Object VMware.Vim.HostDatastoreBrowserSearchSpec
        $spec.Query += New-Object VMware.Vim.FolderFileQuery
        $folderObj = ($dsBrowser.SearchDatastore("[$ds_name] \", $spec)).File | Where-Object { $_.FriendlyName -eq $newFolder }

        # If not, create it
        If ($nil -eq $folderObj) {
            New-Item -ItemType Directory -Path "DS:/$newFolder"
            # Recheck
            $folderObj = ($dsBrowser.SearchDatastore("[$ds_name] \", $spec)).File | Where-Object { $_.FriendlyName -eq $newFolder }
            If ($nil -eq $folderObj) {
                Write-Error -Message "Folder creation failed on $ds_name"
            }
            else {
                Write-Host "Folder creation successful on $ds_name"
            }
        }
        else {
            # Remove old tools files
            Remove-Item -Path "DS:/$newFolder/floppies" -Recurse -ErrorAction SilentlyContinue
            Remove-Item -Path "DS:/$newFolder/vmtools" -Recurse -ErrorAction SilentlyContinue
        }

        Copy-DatastoreItem -Item "./newtools/*" "DS:/$newFolder" -Recurse

        # Remove the PS drive
        Remove-PSDrive -Name DS -Confirm:$false

        # List of hosts attached to that datastore
        $vmhosts = Get-VMHost | Where-Object { $_.ExtensionData.Datastore.value -eq ($ds_id.Id).Split('-', 2)[1] }

        $repo_dir = "/vmfs/volumes/$ds_name/$newFolder"

        # Set the tools-repo
        foreach ($vmhost in $vmhosts) {
            $vmhost.ExtensionData.UpdateProductLockerLocation($repo_dir) | Out-Null
        }

        # Check the tools-repo
        $exist_repo = ($vmhosts | Get-AdvancedSetting -Name "UserVars.ProductLockerLocation" | Select-Object Entity, Value) | Select-Object -Unique
        If (($exist_repo.Value -ne $repo_dir) -or ($exist_repo.count -ne 1)) {
            Write-Error -Message "Failed to set tools-repo on all hosts for datastore $ds_name"
        }
        else {
            Write-Host "Successfully set tools-repo on all hosts for datastore $ds_name"
        }
    }
}

<#
.Synopsis
    Set vSAN compression and deduplication on a cluster or clusters. If deduplication is enabled then compression is required.
    The default cluster configuration is deduplication and compression but the customer can change that.
    Choosing neither compression nor deduplication will disable both.
    This requires action on every physical disk and will take time to complete.
.EXAMPLE
    Set-vSANCompressDedupe -ClustersToChange "cluster-1,cluster-2" -Compression $true
    Set-vSANCompressDedupe -ClustersToChange "cluster-1,cluster-2" -Deduplication $true
    Set-vSANCompressDedupe -ClustersToChange "cluster-1,cluster-2"
    Set-vSANCompressDedupe -ClustersToChange "*"
#>

function Set-vSANCompressDedupe {
    [AVSAttribute(60, UpdatesSDDC = $true)]
    param(
        [Parameter(Mandatory = $true)]
        [String]$ClustersToChange,
        [Parameter(Mandatory = $false,
            HelpMessage = "Enable compression and deduplication.")]
        [bool]$Deduplication,
        [Parameter(Mandatory = $false,
            HelpMessage = "Enable compression only.")]
        [bool]$Compression
    )

    # $cluster is an array of cluster names or "*""
    foreach ($cluster_each in ($ClustersToChange.split(",", [System.StringSplitOptions]::RemoveEmptyEntries)).Trim()) {
        $Clusters += Get-Cluster -Name $cluster_each
    }

    foreach ($Cluster in $Clusters) {
        $cluster_name = $Cluster.Name

        If ($Deduplication) {
            # Deduplication requires compression
            Write-Host "Enabling deduplication and compression on $cluster_name"
            Set-VsanClusterConfiguration -Configuration $cluster_name -SpaceEfficiencyEnabled $true
        }
        elseif ($Compression) {
            # Compression only
            Write-Host "Enabling compression on $cluster_name"
            Set-VsanClusterConfiguration -Configuration $cluster_name -SpaceCompressionEnabled $true
        }
        else {
            # Disable both
            Write-Host "Disabling deduplication and compression on $cluster_name"
            Set-VsanClusterConfiguration -Configuration $cluster_name -SpaceEfficiencyEnabled $false
        }
    }
}

Function Remove-AVSStoragePolicy {
    <#
    .DESCRIPTION
        This function removes a storage policy.
    .PARAMETER Name
        Name of Storage Policy. Wildcards are not supported and will be stripped.
    .EXAMPLE
        Remove-AVSStoragePolicy -Name "Encryption"
    #>


    [CmdletBinding()]
    [AVSAttribute(10, UpdatesSDDC = $false)]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $Name
    )
    Begin {
        #Remove Wildcards characters from Name
        $Name = Limit-WildcardsandCodeInjectionCharacters $Name
        #Protected Policy Object Name Validation Check
        If (Test-AVSProtectedObjectName -Name $Name) {
            Write-Error "$Name is a protected policy name. Please choose a different policy name."
            return
        }

    }
    Process {
        #Get Storage Policy
        $StoragePolicy = Get-SpbmStoragePolicy -Name $Name -ErrorAction SilentlyContinue
        #Remove Storage Policy
        If ([string]::IsNullOrEmpty($StoragePolicy)) {
            Write-Error "Storage Policy $Name does not exist."
            return
        }
        Else { Remove-SpbmStoragePolicy -StoragePolicy $StoragePolicy -Confirm:$false }

    }
}

Function New-AVSStoragePolicy {
    <#
    .DESCRIPTION
        This function creates a new or overwrites an existing vSphere Storage Policy.
        Non vSAN-Based, vSAN Only, VMEncryption Only, Tag Only based and/or any combination of these policy types are supported.
    .PARAMETER Name
        Name of Storage Policy - Wildcards are not allowed and will be stripped.
    .PARAMETER Description
        Description of Storage Policy you are creating, free form text.
    .PARAMETER vSANSiteDisasterTolerance
        Default is "None"
        Valid Values are "None", "Dual", "Preferred", "Secondary", "NoneStretch"
        None = No Site Redundancy (Recommended Option for Non-Stretch Clusters, NOT recommended for Stretch Clusters)
        Dual = Dual Site Redundancy (Recommended Option for Stretch Clusters)
        Preferred = No site redundancy - keep data on Preferred (stretched cluster)
        Secondary = No site redundancy - Keep data on Secondary Site (stretched cluster)
        NoneStretch = No site redundancy - Not Recommended (https://kb.vmware.com/s/article/88358)
        Only valid for stretch clusters.
    .PARAMETER vSANFailuresToTolerate
        Default is "R1FTT1"
        Valid values are "None", "R1FTT1", "R1FTT2", "R1FTT3", "R5FTT1", "R6FTT2", "R1FTT3"
        None = No Data Redundancy
        R1FTT1 = 1 failure - RAID-1 (Mirroring)
        R1FTT2 = 2 failures - RAID-1 (Mirroring)
        R1FTT3 = 3 failures - RAID-1 (Mirroring)
        R5FTT1 = 1 failure - RAID-5 (Erasure Coding)
        R6FTT2 = 2 failures - RAID-6 (Erasure Coding)
        No Data Redundancy options are not covered under Microsoft SLA.
    .PARAMETER VMEncryption
        Default is None. Valid values are None, PreIO, PostIO.
        PreIO allows VAIO filtering solutions to capture data prior to VM encryption.
        PostIO allows VAIO filtering solutions to capture data after VM encryption.
    .PARAMETER vSANObjectSpaceReservation
        Default is 0. Valid values are 0..100
        Object Reservation. 0=Thin Provision, 100=Thick Provision
    .PARAMETER vSANDiskStripesPerObject
        Default is 1. Valid values are 1..12.
        The number of HDDs across which each replica of a storage object is striped.
        A value higher than 1 may result in better performance (for e.g. when flash read cache misses need to get serviced from HDD), but also results in higher use of system resources.
    .PARAMETER vSANIOLimit
        Default is unset. Valid values are 0..2147483647
        IOPS limit for the policy.
    .PARAMETER vSANCacheReservation
        Default is 0. Valid values are 0..100
        Percentage of cache reservation for the policy.
    .PARAMETER vSANChecksumDisabled
        Default is $false. Enable or disable checksum for the policy. Valid values are $true or $false.
        WARNING - Disabling checksum may lead to data LOSS and/or corruption.
        Recommended value is $false.
    .PARAMETER vSANForceProvisioning
        Default is $false. Force provisioning for the policy. Valid values are $true or $false.
        WARNING - vSAN Force Provisioned Objects are not covered under Microsoft SLA. Data LOSS and vSAN instability may occur.
        Recommended value is $false.
    .PARAMETER Tags
        Match to datastores that do have these tags. Tags are case sensitive.
        Comma seperate multiple tags. Example: Tag1,Tag 2,Tag_3
    .PARAMETER NotTags
        Match to datastores that do NOT have these tags. Tags are case sensitive.
        Comma seperate multiple tags. Example: Tag1,Tag 2,Tag_3
    .PARAMETER Overwrite
        Overwrite existing Storage Policy. Default is $false.
        Passing overwrite true provided will overwrite an existing policy exactly as defined.
        Those values not passed will be removed or set to default values.
    .EXAMPLE
        Creates a new storage policy named Encryption with that enables Pre-IO filter VM encryption
        New-AVSStoragePolicy -Name "Encryption" -VMEncryption "PreIO"
    .EXAMPLE
        Creates a new storage policy named "RAID-1 FTT-1 with Pre-IO VM Encryption" with a description enabled for Pre-IO VM Encryption
        New-AVSStoragePolicy -Name "RAID-1 FTT-1 with Pre-IO VM Encryption" -Description "My super secure and performant storage policy" -VMEncryption "PreIO" -vSANFailuresToTolerate "1 failure - RAID-1 (Mirroring)"
    .EXAMPLE
        Creates a new storage policy named "Tagged Datastores" to use datastores tagged with "SSD" and "NVMe" and not datastores tagged "Slow"
        New-AVSStoragePolicy -Name "Tagged Datastores" -Tags "SSD","NVMe" -NotTags "Slow"
    .EXAMPLE
        Creates a new storage policy named "Production Only" to use datastore tagged w/ Production and not tagged w/ Test or Dev. Set with RAID-1, 100% read cache, and Thick Provisioning of Disk.
        New-AVSStoragePolicy -Name "Production Only" -Tags "Production" -NotTags "Test","Dev" -vSANFailuresToTolerate "1 failure - RAID-1 (Mirroring)" -vSANObjectSpaceReservation 100 -vSANCacheReservation 100
    .EXAMPLE
        Passing -Overwrite:$true to any examples provided will overwrite an existing policy exactly as defined. Those values not passed will be removed or set to default values.
        #>

    [CmdletBinding()]
    [AVSAttribute(10, UpdatesSDDC = $false)]
    param(
        #Add parameterSetNames to allow for vSAN, Tags, VMEncryption, StorageIOControl, vSANDirect to be optional.
        [Parameter(Mandatory = $true)]
        [string]
        $Name,
        [Parameter(Mandatory = $false)]
        [string]
        $Description,
        [Parameter(Mandatory = $false)]
        [ValidateSet("None", "Dual", "Preferred", "Secondary", "NoneStretch")]
        [string]
        $vSANSiteDisasterTolerance,
        [Parameter(Mandatory = $false)]
        [ValidateSet("None", "R1FTT1", "R5FTT1", "R1FTT2", "R6FTT2", "R1FTT3")]
        [string]
        $vSANFailuresToTolerate,
        [Parameter(Mandatory = $false)]
        [ValidateSet("None", "PreIO", "PostIO")]
        [string]
        $VMEncryption = "None",
        [Parameter(Mandatory = $false)]
        [ValidateRange(0, 100)]
        [int]
        $vSANObjectSpaceReservation,
        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 12)]
        [int]
        $vSANDiskStripesPerObject,
        [Parameter(Mandatory = $false)]
        [ValidateRange(0, 2147483647)]
        [int]
        $vSANIOLimit,
        [Parameter(Mandatory = $false)]
        [ValidateRange(0, 100)]
        [int]
        $vSANCacheReservation,
        [Parameter(Mandatory = $false)]
        [boolean]
        $vSANChecksumDisabled,
        [Parameter(Mandatory = $false)]
        [boolean]
        $vSANForceProvisioning,
        [Parameter(Mandatory = $false)]
        [string]
        $Tags,
        [Parameter(Mandatory = $false)]
        [string]
        $NotTags,
        [Parameter(Mandatory = $false)]
        [Boolean]
        $Overwrite

    )



    Begin {
        #Cleanup Wildcard and Code Injection Characters
        Write-Information "Cleaning up Wildcard and Code Injection Characters from Name value: $Name"
        $Name = Limit-WildcardsandCodeInjectionCharacters -String $Name
        Write-Information "Name value after cleanup: $Name"
        Write-Information "Cleaning up Wildcard and Code Injection Characters from Description value: $Description"
        If (![string]::IsNullOrEmpty($Description)) { $Description = Limit-WildcardsandCodeInjectionCharacters -String $Description }
        Write-Information "Description value after cleanup: $Description"

        #Protected Policy Object Name Validation Check
        If (Test-AVSProtectedObjectName -Name $Name) {
            Write-Error "$Name is a protected policy name. Please choose a different policy name."
            break
        }

        #Check for existing policy
        $ExistingPolicy = Get-AVSStoragePolicy -Name $Name
        Write-Information ("Existing Policy: " + $ExistingPolicy.name)
        if ($ExistingPolicy -and !$Overwrite) {
            Write-Error "Storage Policy $Name already exists. Set -Overwrite to $true to overwrite existing policy."
            break
        }
        if (!$ExistingPolicy -and $Overwrite) {
            Write-Error "Storage Policy $Name does not exist. Set -Overwrite to $false to create new policy."
            break
        }
        Write-Information "Overwrite value set to: $Overwrite"
        Switch ($Overwrite) {
            $true {
                $pbmprofileresourcetype = new-object vmware.spbm.views.PbmProfileResourceType
                $pbmprofileresourcetype.ResourceType = "STORAGE" # No other known valid value.
                $profilespec = new-object VMware.Spbm.Views.PbmCapabilityProfileUpdateSpec
                $profilespec.Name = $Name
                $profilespec.Constraints = new-object vmware.spbm.views.PbmCapabilitySubProfileConstraints
                If (![string]::IsNullOrEmpty($Description)) { $profilespec.Description = $Description }
            }
            $false {
                $pbmprofileresourcetype = new-object vmware.spbm.views.PbmProfileResourceType
                $pbmprofileresourcetype.ResourceType = "STORAGE" # No other known valid value.
                $profilespec = new-object VMware.Spbm.Views.PbmCapabilityProfileCreateSpec
                $profilespec.ResourceType = $pbmprofileresourcetype
                $profilespec.Name = $Name
                $profilespec.Constraints = new-object vmware.spbm.views.PbmCapabilitySubProfileConstraints
                If (![string]::IsNullOrEmpty($Description)) { $profilespec.Description = $Description }
                $profilespec.Category = "REQUIREMENT" #Valid options are REQUIREMENT = vSAN Storage Policies or RESOURCE = ?? or DATA_SERVICE_POLICY = Common Storage Policies such encryption and storage IO.
                Write-Information "Profile Name set to: $($profilespec.Name)"
                Write-Information "Profile Category set to: $($profilespec.Category)"
            }
        }
        Write-Information "Getting SPBM Capabilities"
        $SPBMCapabilities = Get-AVSSPBMCapabilities
        Foreach ($Capability in $SPBMCapabilities) {
            Write-Information "SPBM Capability: NameSpace: $($Capability.NameSpace), SubCategory: $($Capability.SubCategory), CapabilityMetaData Count: $($Capability.CapabilityMetadata.Count)"
        }

        #vSAN Site Disaster Tolerance / Stretch Cluster specific configuration
        Write-Information "vSANSiteDisasterTolerance value set to: $vSANSiteDisasterTolerance"
        Switch ($vSANSiteDisasterTolerance) {
            "None" {
                #Left blank on purpose. No additional configuration required.
            }
            "Dual" {
                $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
                $Subprofile.Id.Namespace = "VSAN"
                $Subprofile.Id.Id = "subFailuresToTolerate"
                $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
                $Subprofile.Constraint[0].PropertyInstance[0].value = 1
                If (($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).count -eq 0) {
                    $profilespec.Constraints.SubProfiles += new-object VMware.Spbm.Views.PbmCapabilitySubProfile -Property @{"Name" = "VSAN" }
                    Write-Information "Added VSAN Subprofile to ProfileSpec"
                    ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile
                }
                Else { ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile }

                $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
                $Subprofile.Id.Namespace = "VSAN"
                $Subprofile.Id.Id = "locality"
                $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
                $Subprofile.Constraint[0].PropertyInstance[0].value = "None"
                ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile
            }
            "Preferred" {
                $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
                $Subprofile.Id.Namespace = "VSAN"
                $Subprofile.Id.Id = "subFailuresToTolerate"
                $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
                $Subprofile.Constraint[0].PropertyInstance[0].value = 1
                If (($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).count -eq 0) {
                    $profilespec.Constraints.SubProfiles += new-object VMware.Spbm.Views.PbmCapabilitySubProfile -Property @{"Name" = "VSAN" }
                    Write-Information "Added VSAN Subprofile to ProfileSpec"
                    ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile
                }
                Else { ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile }

                $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
                $Subprofile.Id.Namespace = "VSAN"
                $Subprofile.Id.Id = "locality"
                $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
                $Subprofile.Constraint[0].PropertyInstance[0].value = "Preferred Fault Domain"
                If (($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).count -eq 0) {
                    $profilespec.Constraints.SubProfiles += new-object VMware.Spbm.Views.PbmCapabilitySubProfile -Property @{"Name" = "VSAN" }
                    Write-Information "Added VSAN Subprofile to ProfileSpec"
                    ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile
                }
                Else { ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile }
                $Description = $Description + " - Unreplicated objects in a stretch cluster are unprotected by Microsoft SLA and data loss/corruption may occur."
                Write-Warning "$Name policy setting unreplicated objects in a stretch cluster are unprotected by Microsoft SLA and data loss/corruption may occur."
            }
            "Secondary" {
                $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
                $Subprofile.Id.Namespace = "VSAN"
                $Subprofile.Id.Id = "subFailuresToTolerate"
                $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
                $Subprofile.Constraint[0].PropertyInstance[0].value = 1
                If (($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).count -eq 0) {
                    $profilespec.Constraints.SubProfiles += new-object VMware.Spbm.Views.PbmCapabilitySubProfile -Property @{"Name" = "VSAN" }
                    Write-Information "Added VSAN Subprofile to ProfileSpec"
                    ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile
                }
                Else { ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile }

                $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
                $Subprofile.Id.Namespace = "VSAN"
                $Subprofile.Id.Id = "locality"
                $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
                $Subprofile.Constraint[0].PropertyInstance[0].value = "Secondary Fault Domain"
                If (($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).count -eq 0) {
                    $profilespec.Constraints.SubProfiles += new-object VMware.Spbm.Views.PbmCapabilitySubProfile -Property @{"Name" = "VSAN" }
                    Write-Information "Added VSAN Subprofile to ProfileSpec"
                }
                Else { ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile }
                $Description = $Description + " - Unreplicated objects in a stretch cluster are unprotected by Microsoft SLA and data loss/corruption may occur."
                Write-Warning "$Name policy setting unreplicated objects in a stretch cluster are unprotected by Microsoft SLA and data loss/corruption may occur."
            }
            "NoneStretch" {
                $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
                $Subprofile.Id.Namespace = "VSAN"
                $Subprofile.Id.Id = "subFailuresToTolerate"
                $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
                $Subprofile.Constraint[0].PropertyInstance[0].value = 1
                If (($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).count -eq 0) {
                    $profilespec.Constraints.SubProfiles += new-object VMware.Spbm.Views.PbmCapabilitySubProfile -Property @{"Name" = "VSAN" }
                    Write-Information "Added VSAN Subprofile to ProfileSpec"
                }
                Else { ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile }

                $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
                $Subprofile.Id.Namespace = "VSAN"
                $Subprofile.Id.Id = "locality"
                $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
                $Subprofile.Constraint[0].PropertyInstance[0].value = "None"
                If (($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).count -eq 0) {
                    $profilespec.Constraints.SubProfiles += new-object VMware.Spbm.Views.PbmCapabilitySubProfile -Property @{"Name" = "VSAN" }
                    Write-Information "Added VSAN Subprofile to ProfileSpec"
                    ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile
                }
                Else { ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile }
                $Description = $Description + " - Unreplicated objects in a stretch cluster are unprotected by Microsoft SLA and data loss/corruption may occur."
                Write-Warning "$Name policy setting unreplicated objects in a stretch cluster are unprotected by Microsoft SLA and data loss/corruption may occur."
            }
            Default {}
        }
        #vSANFailurestoTolerate / FTT
        Write-Information "vSANFailurestoTolerate value set to: $vSANFailuresToTolerate"
        Switch ($vSANFailuresToTolerate) {
            "None" {
                $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
                $Subprofile.Id.Namespace = "VSAN"
                $Subprofile.Id.Id = "hostFailuresToTolerate"
                $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
                $Subprofile.Constraint[0].PropertyInstance[0].value = 0
                If (($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).count -eq 0) {
                    $profilespec.Constraints.SubProfiles += new-object VMware.Spbm.Views.PbmCapabilitySubProfile -Property @{"Name" = "VSAN" }
                    Write-Information "Added VSAN Subprofile to ProfileSpec"
                    ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile
                }
                Else { ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile }
                $Description = $Description + " - FTT 0 based policy objects are unprotected by Microsoft SLA and data loss/corruption may occur."
                Write-Warning "$Name policy setting $vSANFailurestoTolerate based policy objects are unprotected by Microsoft SLA and data loss/corruption may occur."
            }
            #TODO: Support this?
            "No Data redundancy with host affinity" {  }
            "R1FTT1" {
                $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
                $Subprofile.Id.Namespace = "VSAN"
                $Subprofile.Id.Id = "hostFailuresToTolerate"
                $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
                $Subprofile.Constraint[0].PropertyInstance[0].value = 1
                If (($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).count -eq 0) {
                    $profilespec.Constraints.SubProfiles += new-object VMware.Spbm.Views.PbmCapabilitySubProfile -Property @{"Name" = "VSAN" }
                    Write-Information "Added VSAN Subprofile to ProfileSpec"
                    ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile
                }
                Else { ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile }

                $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
                $Subprofile.Id.Namespace = "VSAN"
                $Subprofile.Id.Id = "replicaPreference"
                $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
                $Subprofile.Constraint[0].PropertyInstance[0].value = "RAID-1 (Mirroring) - Performance"
                ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile
            }
            "R5FTT1" {
                $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
                $Subprofile.Id.Namespace = "VSAN"
                $Subprofile.Id.Id = "hostFailuresToTolerate"
                $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
                $Subprofile.Constraint[0].PropertyInstance[0].value = 1
                If (($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).count -eq 0) {
                    $profilespec.Constraints.SubProfiles += new-object VMware.Spbm.Views.PbmCapabilitySubProfile -Property @{"Name" = "VSAN" }
                    Write-Information "Added VSAN Subprofile to ProfileSpec"
                    ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile
                }
                Else { ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile }

                Write-Information "Profilespec: $($profilespec | Out-String)"
                $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
                $Subprofile.Id.Namespace = "VSAN"
                $Subprofile.Id.Id = "replicaPreference"
                $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
                $Subprofile.Constraint[0].PropertyInstance[0].value = "RAID-5/6 (Erasure Coding) - Capacity"
                ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile

                Write-Information "Profilespec: $($profilespec | Out-String)"
                $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
                $Subprofile.Id.Namespace = "VSAN"
                $Subprofile.Id.Id = "storageType"
                $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
                $Subprofile.Constraint[0].PropertyInstance[0].value = "Allflash"
                ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile
                Write-Information "All Flash added to ProfileSpec as required for $vsanFailurestoTolerate"
            }
            "R1FTT2" {
                $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
                $Subprofile.Id.Namespace = "VSAN"
                $Subprofile.Id.Id = "hostFailuresToTolerate"
                $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
                $Subprofile.Constraint[0].PropertyInstance[0].value = 2
                If (($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).count -eq 0) {
                    $profilespec.Constraints.SubProfiles += new-object VMware.Spbm.Views.PbmCapabilitySubProfile -Property @{"Name" = "VSAN" }
                    Write-Information "Added VSAN Subprofile to ProfileSpec"
                    ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile
                }
                Else { ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile }

                $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
                $Subprofile.Id.Namespace = "VSAN"
                $Subprofile.Id.Id = "replicaPreference"
                $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
                $Subprofile.Constraint[0].PropertyInstance[0].value = "RAID-1 (Mirroring) - Performance"
                ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile
            }
            "R6FTT2" {
                $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
                $Subprofile.Id.Namespace = "VSAN"
                $Subprofile.Id.Id = "hostFailuresToTolerate"
                $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
                $Subprofile.Constraint[0].PropertyInstance[0].value = 2
                If (($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).count -eq 0) {
                    $profilespec.Constraints.SubProfiles += new-object VMware.Spbm.Views.PbmCapabilitySubProfile -Property @{"Name" = "VSAN" }
                    Write-Information "Added VSAN Subprofile to ProfileSpec"
                    ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile
                }
                Else { ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile }

                $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
                $Subprofile.Id.Namespace = "VSAN"
                $Subprofile.Id.Id = "replicaPreference"
                $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
                $Subprofile.Constraint[0].PropertyInstance[0].value = "RAID-5/6 (Erasure Coding) - Capacity"
                ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile

                $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
                $Subprofile.Id.Namespace = "VSAN"
                $Subprofile.Id.Id = "storageType"
                $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
                $Subprofile.Constraint[0].PropertyInstance[0].value = "Allflash"
                ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile
                Write-Information "All Flash added to ProfileSpec as required for $vsanFailurestoTolerate"
            }
            "R1FTT3" {
                $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
                $Subprofile.Id.Namespace = "VSAN"
                $Subprofile.Id.Id = "hostFailuresToTolerate"
                $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
                $Subprofile.Constraint[0].PropertyInstance[0].value = 3
                If (($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).count -eq 0) {
                    $profilespec.Constraints.SubProfiles += new-object VMware.Spbm.Views.PbmCapabilitySubProfile -Property @{"Name" = "VSAN" }
                    Write-Information "Added VSAN Subprofile to ProfileSpec"
                    ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile
                }
                Else { ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile }

                $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
                $Subprofile.Id.Namespace = "VSAN"
                $Subprofile.Id.Id = "replicaPreference"
                $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
                $Subprofile.Constraint[0].PropertyInstance[0].value = "RAID-1 (Mirroring) - Performance"
                ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile
            }
            Default {}
        }
        #vSANChecksumDisabled
        Write-Information "vSANChecksumDisabled value is: $vSANChecksumDisabled"
        Switch ($vSANChecksumDisabled) {
            $true {
                $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
                $Subprofile.Id.Namespace = "VSAN"
                $Subprofile.Id.Id = "checksumDisabled"
                $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
                $Subprofile.Constraint[0].PropertyInstance[0].value = $true
                If (($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).count -eq 0) {
                    $profilespec.Constraints.SubProfiles += new-object VMware.Spbm.Views.PbmCapabilitySubProfile -Property @{"Name" = "VSAN" }
                    Write-Information "Added VSAN Subprofile to ProfileSpec"
                }
                Else { ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile }
                $Description = $Description + " - Disabling vSAN Checksum may invalidate Microsoft SLA terms and data loss/corruption may occur."
                Write-Warning "Disabling vSAN Checksum may invalidate Microsoft SLA terms and data loss/corruption may occur."
            }
            # Empty profile spec defaults to setting to false in overwrite case
            $false {}
        }
        #vSANForceProvisioning
        Write-Information "vSANForceProvisioning Value is: $vSANForceProvisioning"
        Switch ($vSANForceProvisioning) {
            $true {
                $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
                $Subprofile.Id.Namespace = "VSAN"
                $Subprofile.Id.Id = "forceProvisioning"
                $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
                $Subprofile.Constraint[0].PropertyInstance[0].value = $true
                If (($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).count -eq 0) {
                    $profilespec.Constraints.SubProfiles += new-object VMware.Spbm.Views.PbmCapabilitySubProfile -Property @{"Name" = "VSAN" }
                    Write-Information "Added VSAN Subprofile to ProfileSpec"
                    ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile
                }
                Else { ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile }
                $Description = $Description + " - Force Provisioned objects are unprotected by Microsoft SLA and data loss/corruption may occur."
                Write-Warning "$Name policy setting Force Provisioned objects are unprotected by Microsoft SLA and data loss/corruption may occur."
            }
            # Empty profile spec defaults to setting to false in overwrite case
            $false {}
        }

        #vSANDiskStripesPerObject
        Write-Information "vSANDiskStripesPerObject value is: $vSANDiskStripesPerObject"
        If ($vSANDiskStripesPerObject -gt 0) {
            Write-Information "Creating vSAN Disk Stripes Subprofile"
            $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
            $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
            $Subprofile.Id.Namespace = "VSAN"
            $Subprofile.Id.Id = "stripeWidth"
            $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
            $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
            $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
            $Subprofile.Constraint[0].PropertyInstance[0].value = $vSANDiskStripesPerObject
            If (($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).count -eq 0) {
                $profilespec.Constraints.SubProfiles += new-object VMware.Spbm.Views.PbmCapabilitySubProfile -Property @{"Name" = "VSAN" }
                Write-Information "Added VSAN Subprofile to ProfileSpec"
                ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile
            }
            Else { ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile }
        }

        #VSANIOLimit
        Write-Information "vSANIOLimit set to: $vSANIOLimit"
        If ($vSANIOLimit -gt 0) {
            Write-Information "Building vSAN IOLimit Subprofile"
            $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
            $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
            $Subprofile.Id.Namespace = "VSAN"
            $Subprofile.Id.Id = "iopsLimit"
            $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
            $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
            $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
            $Subprofile.Constraint[0].PropertyInstance[0].value = $vSANIOLimit
            If (($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).count -eq 0) {
                $profilespec.Constraints.SubProfiles += new-object VMware.Spbm.Views.PbmCapabilitySubProfile -Property @{"Name" = "VSAN" }
                Write-Information "Added VSAN Subprofile to ProfileSpec"
                ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile
            }
            Else { ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile }
        }

        #VSANCacheReservation
        Write-Information "vSANCacheReservation set to: $vSANCacheReservation"
        If ($vSANCacheReservation -gt 0) {
            Write-Information "Creating vSANCacheReservation Subprofile"
            $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
            $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
            $Subprofile.Id.Namespace = "VSAN"
            $Subprofile.Id.Id = "cacheReservation"
            $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
            $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
            $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
            $Subprofile.Constraint[0].PropertyInstance[0].value = ([int]$vSANCacheReservation * 10000)
            If (($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).count -eq 0) {
                $profilespec.Constraints.SubProfiles += new-object VMware.Spbm.Views.PbmCapabilitySubProfile -Property @{"Name" = "VSAN" }
                Write-Information "Added VSAN Subprofile to ProfileSpec"
                ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile
            }
            Else { ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile }
        }

        #VSANObjectReservation
        Write-Information "vSANObjectReservation set to: $vSANObjectSpaceReservation"
        If ($vSANObjectSpaceReservation -gt 0) {
            Write-Information "Creating vSANObjectReservation Subprofile"
            $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
            $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
            $Subprofile.Id.Namespace = "VSAN"
            $Subprofile.Id.Id = "proportionalCapacity"
            $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
            $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
            $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
            $Subprofile.Constraint[0].PropertyInstance[0].value = $vSANObjectSpaceReservation
            If (($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).count -eq 0) {
                $profilespec.Constraints.SubProfiles += new-object VMware.Spbm.Views.PbmCapabilitySubProfile -Property @{"Name" = "VSAN" }
                Write-Information "Added VSAN Subprofile to ProfileSpec"
                ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile
            }
            Else { ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "VSAN" }).Capability += $subprofile }
        }

        # Tag Support for Storage Policies
        Write-Information ("Tags recorded as: " + $Tags)
        $TagData = $SPBMCapabilities | Where-Object { $_.subcategory -eq "Tag" }
        If (![string]::IsNullOrEmpty($Tags)) {
            # Needed as run command does not support string array types, cannot simply overwrite existing variable for some reason.
            $Array = Convert-StringToArray -String $Tags
            Foreach ($Tag in $Array) {
                Write-Information ("Tag: " + $Tag)
                $Tag = Limit-WildcardsandCodeInjectionCharacters -String $Tag
                $ObjectTag = Get-Tag -Name $Tag
                If (![string]::IsNullOrEmpty($ObjectTag)) {
                    If ($ObjectTag.count -gt 1) {
                        Write-Information "Multiple Tags found with the name $Tag. Filtering by Datastore category."
                        Foreach ($Entry in $ObjectTag) {
                            Write-Information ("Tag Name: " + $Entry.Name)
                            If ($Entry.Category.EntityType -eq "Datastore") {
                                $CatData = $TagData.CapabilityMetadata | Where-Object { $_.summary.Label -eq $Entry.Category.Name }
                                $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                                $Subprofile.Id = $Catdata.Id
                                $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                                $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                                $Subprofile.Constraint[0].PropertyInstance[0].id = $Catdata.propertymetadata.id
                                $Subprofile.Constraint[0].PropertyInstance[0].Operator = ""
                                $Subprofile.Constraint[0].PropertyInstance[0].value = New-object VMware.Spbm.Views.PbmCapabilityDiscreteSet
                                $Subprofile.Constraint[0].PropertyInstance[0].value.values = $Entry.Name
                                If (($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "Tag based placement" }).count -eq 0) {
                                    $profilespec.Constraints.SubProfiles += new-object VMware.Spbm.Views.PbmCapabilitySubProfile -Property @{"Name" = "Tag based placement" }
                                    Write-Information "Added Tag based placement subprofile to ProfileSpec"
                                    ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "Tag based placement" }).Capability += $Subprofile
                                    Write-Information "Added $Tag to profilespec"
                                }
                                Else {
                                    ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "Tag based placement" }).Capability += $Subprofile
                                }

                            }
                            If ($Entry.Category.EntityType -ne "Datastore") {
                                Write-Information "Tag $($Entry.Name) of category $($Entry.Category.Name) is not a Datastore Tag. Skipping."
                            }
                        }
                    }
                    If ($ObjectTag.count -eq 1) {
                        If ($ObjectTag.Category.EntityType -ne "Datastore") {
                            Write-Warning "Tag $Tag is not a Datastore Tag. Skipping."
                        }
                        Else {
                            $Entry = $ObjectTag
                            $CatData = $TagData.CapabilityMetadata | Where-Object { $_.summary.Label -eq $Entry.Category.Name }
                            $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                            $Subprofile.Id = $Catdata.Id
                            $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                            $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                            $Subprofile.Constraint[0].PropertyInstance[0].id = $Catdata.propertymetadata.id
                            $Subprofile.Constraint[0].PropertyInstance[0].Operator = ""
                            $Subprofile.Constraint[0].PropertyInstance[0].value = New-object VMware.Spbm.Views.PbmCapabilityDiscreteSet
                            $Subprofile.Constraint[0].PropertyInstance[0].value.values = $Entry.Name
                            If (($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "Tag based placement" }).count -eq 0) {
                                $profilespec.Constraints.SubProfiles += new-object VMware.Spbm.Views.PbmCapabilitySubProfile -Property @{"Name" = "Tag based placement" }
                                Write-Information "Added Tag based placement subprofile to ProfileSpec"
                                    ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "Tag based placement" }).Capability += $Subprofile
                                Write-Information "Added $Tag to profilespec"
                            }
                            Else {
                                ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "Tag based placement" }).Capability += $Subprofile
                            }
                        }


                    }


                }
                Else { Write-Error "Tag $Tag not found. Skipping. Tags are case-sensitive, please verify." }
            }


        }

        # Not Tag Support for Storage Policies
        Write-Information ("NotTags recorded as: " + $NotTags)
        If (![string]::IsNullOrEmpty($NotTags)) {
            # Needed as run command does not support string array types, cannot simply overwrite existing variable for some reason.
            $Array = Convert-StringToArray -String $NotTags
            Foreach ($Tag in $Array) {
                Write-Information ("Tag: " + $Tag)
                $Tag = Limit-WildcardsandCodeInjectionCharacters -String $Tag
                $ObjectTag = Get-Tag -Name $Tag
                If (![string]::IsNullOrEmpty($ObjectTag)) {
                    If ($ObjectTag.count -gt 1) {
                        Write-Information "Multiple Tags found with the name $Tag. Filtering by Datastore category."
                        Foreach ($Entry in $ObjectTag) {
                            Write-Information ("Tag Name: " + $Entry.Name)
                            If ($Entry.Category.EntityType -eq "Datastore") {
                                $CatData = $TagData.CapabilityMetadata | Where-Object { $_.summary.Label -eq $Entry.Category.Name }
                                $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                                $Subprofile.Id = $Catdata.Id
                                $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                                $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                                $Subprofile.Constraint[0].PropertyInstance[0].id = $Catdata.propertymetadata.id
                                $Subprofile.Constraint[0].PropertyInstance[0].Operator = "NOT"
                                $Subprofile.Constraint[0].PropertyInstance[0].value = New-object VMware.Spbm.Views.PbmCapabilityDiscreteSet
                                $Subprofile.Constraint[0].PropertyInstance[0].value.values = $Entry.Name
                                If (($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "Tag based placement" }).count -eq 0) {
                                    $profilespec.Constraints.SubProfiles += new-object VMware.Spbm.Views.PbmCapabilitySubProfile -Property @{"Name" = "Tag based placement" }
                                    Write-Information "Added Tag based placement subprofile to ProfileSpec"
                                    ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "Tag based placement" }).Capability += $Subprofile
                                    Write-Information "Added $Tag to profilespec"
                                }
                                Else {
                                    ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "Tag based placement" }).Capability += $Subprofile
                                }

                            }
                            If ($Entry.Category.EntityType -ne "Datastore") {
                                Write-Information "Tag $($Entry.Name) of category $($Entry.Category.Name) is not a Datastore Tag. Skipping."
                            }
                        }
                    }
                    If ($ObjectTag.count -eq 1) {
                        if ($ObjectTag.Category.EntityType -ne "Datastore") {
                            Write-Information "Tag $Tag is not a Datastore Tag. Skipping."
                        }
                        Else {
                            $Entry = $ObjectTag
                            $CatData = $TagData.CapabilityMetadata | Where-Object { $_.summary.Label -eq $Entry.Category.Name }
                            $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                            $Subprofile.Id = $Catdata.Id
                            $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                            $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                            $Subprofile.Constraint[0].PropertyInstance[0].id = $Catdata.propertymetadata.id
                            $Subprofile.Constraint[0].PropertyInstance[0].Operator = "NOT"
                            $Subprofile.Constraint[0].PropertyInstance[0].value = New-object VMware.Spbm.Views.PbmCapabilityDiscreteSet
                            $Subprofile.Constraint[0].PropertyInstance[0].value.values = $Entry.Name
                            If (($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "Tag based placement" }).count -eq 0) {
                                $profilespec.Constraints.SubProfiles += new-object VMware.Spbm.Views.PbmCapabilitySubProfile -Property @{"Name" = "Tag based placement" }
                                Write-Information "Added Tag based placement subprofile to ProfileSpec"
                                ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "Tag based placement" }).Capability += $Subprofile
                                Write-Information "Added $Tag to profilespec"
                            }
                            Else {
                                    ($profilespec.Constraints.SubProfiles | Where-Object { $_.Name -eq "Tag based placement" }).Capability += $Subprofile
                            }
                        }


                    }


                }
                Else { Write-Error "Tag $Tag not found. Skipping. Tags are case-sensitive, please verify." }
            }


        }
        #IMPORTANT - Any additional functionality should be added before the VMEncryption Parameter. The reason is that this subprofile must be added as a capability to all subprofile types for API to accept.
        Write-Information "VMEncryption set to: $VMEncryption"
        Switch ($VMEncryption) {
            "None" {}
            "PreIO" {
                #Check for AVS VM Encryption Policies, create if not present.
                $IOPolicy = Get-AVSStoragePolicy -Name "AVS PRE IO Encryption" -ResourceType "DATA_SERVICE_POLICY"
                If (!$IOPolicy) { $IOPolicy = New-AVSCommonStoragePolicy -Encryption -Name "AVS PRE IO Encryption" -Description "Encrypts VM before VAIO Filter" -PostIOEncryption $false }
                Write-Information ("VMEncryption uniqueID: " + $IOPolicy.ProfileId.UniqueId)
                $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
                $Subprofile.Id.Namespace = "com.vmware.storageprofile.dataservice"
                $Subprofile.Id.Id = $IOPolicy.ProfileId.UniqueId
                $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
                $Subprofile.Constraint[0].PropertyInstance[0].value = $Subprofile.Id.Id
                If ($profilespec.Constraints.SubProfiles.count -eq 0) {
                    $SubprofileName = "Host based services"
                    $profilespec.Constraints.SubProfiles += new-object VMware.Spbm.Views.PbmCapabilitySubProfile -Property @{"Name" = $SubprofileName }
                    Write-Information "Added $SubprofileName to ProfileSpec"
                    Foreach ($service in $profilespec.Constraints.SubProfiles) {
                        $service.Capability += $subprofile
                    }
                }
                ElseIf ($profilespec.Constraints.SubProfiles.count -ge 1) {
                    Foreach ($service in $profilespec.Constraints.SubProfiles) {
                        $service.Capability += $subprofile
                    }
                }
                Write-Information "Added $($IOPolicy.Name) to profilespec"

            }
            "PostIO" {
                $IOPolicy = Get-AVSStoragePolicy -Name "AVS POST IO Encryption" -ResourceType "DATA_SERVICE_POLICY"
                If (!$IOPolicy) { $IOPolicy = New-AVSCommonStoragePolicy -Encryption -Name "AVS POST IO Encryption" -Description "Encrypts VM after VAIO Filter" -PostIOEncryption $true }
                Write-Information ("VMEncryption uniqueID: " + $IOPolicy.ProfileId.UniqueId)
                $Subprofile = new-object VMware.Spbm.Views.PbmCapabilityInstance
                $Subprofile.Id = New-Object VMware.Spbm.Views.PbmCapabilityMetadataUniqueId
                $Subprofile.Id.Namespace = "com.vmware.storageprofile.dataservice"
                $Subprofile.Id.Id = $IOPolicy.profileid.UniqueId
                $Subprofile.Constraint = New-Object VMware.Spbm.Views.PbmCapabilityConstraintInstance
                $Subprofile.Constraint[0].PropertyInstance = New-Object VMware.Spbm.Views.PbmCapabilityPropertyInstance
                $Subprofile.Constraint[0].PropertyInstance[0].id = $Subprofile.Id.Id
                $Subprofile.Constraint[0].PropertyInstance[0].value = $Subprofile.Id.Id
                If ($profilespec.Constraints.SubProfiles.count -eq 0) {
                    $SubprofileName = "Host based services"
                    $profilespec.Constraints.SubProfiles += new-object VMware.Spbm.Views.PbmCapabilitySubProfile -Property @{"Name" = $SubprofileName }
                    Write-Information "Added $SubprofileName to ProfileSpec"
                    Write-Information $profilespec.Constraints.SubProfiles[0].Name
                    Foreach ($service in $profilespec.Constraints.SubProfiles) {
                        $service.Capability += $subprofile
                    }
                }
                ElseIf ($profilespec.Constraints.SubProfiles.count -ge 1) {
                    Foreach ($service in $profilespec.Constraints.SubProfiles) {
                        $service.Capability += $subprofile
                    }
                }
                Write-Information "Added $($IOPolicy.Name) to profilespec"

            }
            Default {}
        }

    }
    process {
        $profilespec.Description = $Description
        #return $profilespec #Uncomment to capture and debug profile spec.
        If ($profilespec.Constraints.SubProfiles.count -eq 0) {
            Write-Error "At least one parameter must be defined to create a storage policy."
            Return
        }
        $serviceInstanceView = Get-SpbmView -Id "PbmServiceInstance-ServiceInstance"
        $spbmServiceContent = $serviceInstanceView.PbmRetrieveServiceContent()
        $spbmProfMgr = Get-SpbmView -Id $spbmServiceContent.ProfileManager
        If ($Overwrite) {
            $spbmProfMgr.PbmUpdate($ExistingPolicy.ProfileId, $profilespec)
            if ($?) { return "$($ExistingPolicy.Name) Updated" }
            else { return "$($ExistingPolicy.Name) Update Failed" }

        }
        Else {
            $profileuniqueID = $spbmProfMgr.PbmCreate($profilespec)
            $existingpolicies = Get-AVSStoragePolicy
            $createdpolicy = $existingpolicies | where-object { $_.profileid.uniqueid -eq $profileuniqueID.UniqueId }
            Write-Information "Created $($createdpolicy.Name)"
            return ("Created " + $createdpolicy.Name + " " + $profileuniqueID.UniqueId)
        }

    }
}

<#
    .Synopsis
        This allows the customer to change DRS from the default setting to 1-4 with 4 being the least conservative.
    .PARAMETER Drs
        The DRS setting to apply to the cluster. 3 is the default setting, 2 is one step more conservative (meaning less agressive in moving VMs).
    .PARAMETER ClustersToChange
        The clusters to apply the DRS setting to. This can be a single cluster or a comma separated list of clusters or a wildcard.
    .EXAMPLE
        Set-CustomDRS -ClustersToChange "Cluster-1, Cluster-2" -Drs 2
        Set-CustomDRS -ClustersToChange "*" -Drs 3 # This returns it to the default setting
#>

function Set-CustomDRS {

    [AVSAttribute(15, UpdatesSDDC = $false)]
    param(
        [Parameter(Mandatory = $true)]
        [String]$ClustersToChange,
        [Parameter(Mandatory = $true,
            HelpMessage = "The DRS setting. Default of 3 or more conservative of 2 or less conservative 4.")]
        [ValidateRange(1, 4)]
        [int] $Drs
    )

    switch ($Drs) {
        4 { $drsChange = 2 }
        3 { $drsChange = 3 }
        2 { $drsChange = 4 }
        1 { $drsChange = 5 }
        Default { $drsChange = 3 }
    }

    # Settings for DRS
    $spec = New-Object VMware.Vim.ClusterConfigSpecEx
    $spec.DrsConfig = New-Object VMware.Vim.ClusterDrsConfigInfo
    $spec.DrsConfig.VmotionRate = $drsChange
    $spec.DrsConfig.Enabled = $true
    $spec.DrsConfig.Option = New-Object VMware.Vim.OptionValue[] (2)
    $spec.DrsConfig.Option[0] = New-Object VMware.Vim.OptionValue
    $spec.DrsConfig.Option[0].Value = '0'
    $spec.DrsConfig.Option[0].Key = 'TryBalanceVmsPerHost'
    $spec.DrsConfig.Option[1] = New-Object VMware.Vim.OptionValue
    $spec.DrsConfig.Option[1].Value = '1'
    $spec.DrsConfig.Option[1].Key = 'IsClusterManaged'
    $modify = $true
    # End DRS settings

    # $cluster is an array of cluster names or "*""
    foreach ($cluster_each in ($ClustersToChange.split(",", [System.StringSplitOptions]::RemoveEmptyEntries)).Trim()) {
        $Clusters += Get-Cluster -Name $cluster_each
    }

    foreach ($cluster in $clusters) {
        try {
            $_this = Get-View -Id $cluster.Id
            $_this.ReconfigureComputeResource_Task($spec, $modify)
            Write-Host "Successfully set DRS for cluster $($cluster.Name)."
        }
        catch {
            Write-Error "Failed to set DRS for cluster $($cluster.Name)."
        }
    }
}

Function Set-AVSVSANClusterUNMAPTRIM {
    <#
    .DESCRIPTION
        This function enables vSAN UNMAP/TRIM on the cluster defined by the -Name parameter.
        Once enabled, supported Guest OS VM's must be powered off and powered back on. A reboot will not suffice.
        See url for more information: https://core.vmware.com/resource/vsan-space-efficiency-technologies#sec19560-sub6
    .PARAMETER Name
        Name of Clusters as defined in vCenter. Valid values are blank or a comma separated list of cluster names.
        Set-AVSVSANClusterUNMAPTRIM -Name Cluster-1,Cluster-2,Cluster-3
        Enables UNMAP/TRIM on Clusters-1,2,3
        Set-AVSVSANClusterUNMAPTRIM -Enable:True
        Enables UNMAP/TRIM on all Clusters
    .PARAMETER Enable
        Set to true to enable UNMAP/TRIM on target cluster(s). Default is false.
        WARNING - There is a performance impact when UNMAP/TRIM is enabled.
        See url for more information: https://core.vmware.com/resource/vsan-space-efficiency-technologies#sec19560-sub6
    .EXAMPLE
        Set-AVSVSANClusterUNMAPTRIM -Name 'Cluster-1,Cluster-2,Cluster-3'
        Enables UNMAP/TRIM on Clusters-1,2,3
    .EXAMPLE
        Set-AVSVSANClusterUNMAPTRIM -Enable:True
        Enables UNMAP/TRIM on all Clusters
    #>


    [CmdletBinding()]
    [AVSAttribute(10, UpdatesSDDC = $false)]
    param (
        [Parameter(Mandatory = $false)]
        [string]
        $Name,
        [Parameter(Mandatory = $true)]
        [bool]
        $Enable
    )
    begin {
        If ([string]::IsNullOrEmpty($Name)){}
        Else {
            $Name = Limit-WildcardsandCodeInjectionCharacters -String $Name
            $Array = Convert-StringToArray -String $Name
        }
        $TagName = "VSAN UNMAP/TRIM"
        $InfoMessage = "Info - There may be a performance impact when UNMAP/TRIM is enabled.
            See url for more information: https://core.vmware.com/resource/vsan-space-efficiency-technologies#sec19560-sub6"

    }
    process {
        If ([string]::IsNullOrEmpty($Array)) {
            $Clusters = Get-Cluster
            Foreach ($Cluster in $Clusters) {
                $Cluster | Set-VsanClusterConfiguration -GuestTrimUnmap:$Enable
                Add-AVSTag -Name $TagName -Description $InfoMessage -Entity $Cluster
                Write-Information "$($Cluster.Name) set to $Enabled for UNMAP/TRIM"
                If ($Enable) {
                    Write-Information $InfoMessage
                }
            }
            Get-Cluster | Set-VsanClusterConfiguration -GuestTrimUnmap:$Enable
        }
        Else {
            Foreach ($Entry in $Array) {
                If ($Cluster = Get-Cluster -name $Entry) {
                    $Cluster | Set-VsanClusterConfiguration -GuestTrimUnmap:$Enable
                    Write-Information "$($Cluster.Name) set to $Enabled for UNMAP/TRIM"
                    If ($Enable) {
                        Write-Information $InfoMessage
                        Add-AVSTag -Name $TagName -Description $InfoMessage -Entity $Cluster
                    }
                    If ($Enable -eq $false) {
                        $AssignedTag = Get-TagAssignment -Tag $Tagname -Entity $Cluster
                        Remove-TagAssignment -TagAssignment $AssignedTag -Confirm:$false
                    }
                }
            }
        }
    }
}

Function Get-AVSVSANClusterUNMAPTRIM {
    <#
    .DESCRIPTION
        This function gets vSAN UNMAP/TRIM configuration status on all clusters.
    #>


    [CmdletBinding()]
    [AVSAttribute(10, UpdatesSDDC = $false)]
    param ()
    begin {}
    process {
            Get-Cluster | Get-VsanClusterConfiguration | Select-Object Name, GuestTrimUnmap
        }
}

function Remove-CustomRole {
    <#
    .DESCRIPTION
        This function allows customer to remove a custom role from the SDDC.
        Useful in case of roles created with greater privileges than Cloudadmin that can no longer be removed from the UI.
    #>


    [CmdletBinding()]
    [AVSAttribute(10, UpdatesSDDC = $false)]
    param (
        [Parameter(Mandatory = $true,
            HelpMessage = "The name of the role to remove, as displayed in the vCenter UI (case insensitive). This must be a custom role.")]
        [string]
        $roleInput
    )
    # Check if the role exists before attempting removal
    $roleToRemove = Get-VIRole | Where-Object { $_.Description -eq $roleInput }

    # Check if the role is in the protected names list or is a System role
    if ($roleToRemove.Count -eq 1) {
        if ((Test-AVSProtectedObjectName -Name $roleToRemove.Name) -or $roleToRemove.IsSystem -eq $true) {
            Write-Error "'$roleInput' is either System or Built-in. Removal not allowed."
        }
        else {
            try {
                Remove-VIRole -Role $roleToRemove -Confirm:$false -Force:$false
                Write-Host "The role '$roleInput' has been removed."
            }
            catch {
                Write-Error "Failed to remove the role '$roleInput'."
                Write-Error $_.Exception.Message
            }
        }
    }
    else {
        Write-Host "The role '$roleInput' was not found or can refer to several roles. No removal performed. Below the list of roles found:"
        foreach ($roleItem in $roleToRemove) {
            Write-Host "Role Name: $($roleItem.Name)"
            Write-Host "Role Description: $($roleItem.Description)"
        }
    }
}


# SIG # Begin signature block
# MIIoRgYJKoZIhvcNAQcCoIIoNzCCKDMCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAb05DrtCZd4/ZF
# JsuUFarAgj4e5arZI3MCO3MeB+MhAKCCDXYwggX0MIID3KADAgECAhMzAAAEBGx0
# Bv9XKydyAAAAAAQEMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjQwOTEyMjAxMTE0WhcNMjUwOTExMjAxMTE0WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQC0KDfaY50MDqsEGdlIzDHBd6CqIMRQWW9Af1LHDDTuFjfDsvna0nEuDSYJmNyz
# NB10jpbg0lhvkT1AzfX2TLITSXwS8D+mBzGCWMM/wTpciWBV/pbjSazbzoKvRrNo
# DV/u9omOM2Eawyo5JJJdNkM2d8qzkQ0bRuRd4HarmGunSouyb9NY7egWN5E5lUc3
# a2AROzAdHdYpObpCOdeAY2P5XqtJkk79aROpzw16wCjdSn8qMzCBzR7rvH2WVkvF
# HLIxZQET1yhPb6lRmpgBQNnzidHV2Ocxjc8wNiIDzgbDkmlx54QPfw7RwQi8p1fy
# 4byhBrTjv568x8NGv3gwb0RbAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQU8huhNbETDU+ZWllL4DNMPCijEU4w
# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW
# MBQGA1UEBRMNMjMwMDEyKzUwMjkyMzAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci
# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG
# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0
# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAIjmD9IpQVvfB1QehvpC
# Ge7QeTQkKQ7j3bmDMjwSqFL4ri6ae9IFTdpywn5smmtSIyKYDn3/nHtaEn0X1NBj
# L5oP0BjAy1sqxD+uy35B+V8wv5GrxhMDJP8l2QjLtH/UglSTIhLqyt8bUAqVfyfp
# h4COMRvwwjTvChtCnUXXACuCXYHWalOoc0OU2oGN+mPJIJJxaNQc1sjBsMbGIWv3
# cmgSHkCEmrMv7yaidpePt6V+yPMik+eXw3IfZ5eNOiNgL1rZzgSJfTnvUqiaEQ0X
# dG1HbkDv9fv6CTq6m4Ty3IzLiwGSXYxRIXTxT4TYs5VxHy2uFjFXWVSL0J2ARTYL
# E4Oyl1wXDF1PX4bxg1yDMfKPHcE1Ijic5lx1KdK1SkaEJdto4hd++05J9Bf9TAmi
# u6EK6C9Oe5vRadroJCK26uCUI4zIjL/qG7mswW+qT0CW0gnR9JHkXCWNbo8ccMk1
# sJatmRoSAifbgzaYbUz8+lv+IXy5GFuAmLnNbGjacB3IMGpa+lbFgih57/fIhamq
# 5VhxgaEmn/UjWyr+cPiAFWuTVIpfsOjbEAww75wURNM1Imp9NJKye1O24EspEHmb
# DmqCUcq7NqkOKIG4PVm3hDDED/WQpzJDkvu4FrIbvyTGVU01vKsg4UfcdiZ0fQ+/
# V0hf8yrtq9CkB8iIuk5bBxuPMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq
# 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
# /Xmfwb1tbWrJUnMTDXpQzTGCGiYwghoiAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBIDIwMTECEzMAAAQEbHQG/1crJ3IAAAAABAQwDQYJYIZIAWUDBAIB
# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIPCxlXLuOAz6FT5Hm26rBg8Z
# OLEXJMd5C74El8N4Ts2xMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A
# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB
# BQAEggEAB4rq/C/oXjDOruixeG4vg1MSAQ2X9hRFCFQyYfrkO2GfPSamF5vht9bU
# h8qRYTS6Awwy8w2xN6Pu2hAVRbxG6QP7c73Jx/rndyRfA9W/C7Yvu7Cup6/Xc7kH
# v+8YKX2rfggl5GVaxa0TqgFPRyMQaHTKlIOIBg+GpNKxXcawzOECK5MAbZjHY+jR
# yZRoGWsm9LN1EHKvr/Ie4vc95jXjPg+WqMmB/pnjKCuStV9/rH1z+0zELOEZ9yCg
# gnhyTlK+uCawwXclAbRXOhtc9xA+YT05HBk2dORTrFtpz/sKDv7U4z2VfyHw5sYE
# LmmkhTyutt4//nT7GSIt9sgTzZO6O6GCF7AwghesBgorBgEEAYI3AwMBMYIXnDCC
# F5gGCSqGSIb3DQEHAqCCF4kwgheFAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsq
# hkiG9w0BCRABBKCCAUkEggFFMIIBQQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl
# AwQCAQUABCB/o8R0zVpBmdk4loAwyH6lJ9TPvkgyoJWfwJQjAtqReAIGZut4SWSb
# GBMyMDI0MTAzMTIwMDg1NS42MTNaMASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl
# bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVT
# Tjo2NTFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAg
# U2VydmljZaCCEf4wggcoMIIFEKADAgECAhMzAAAB9ZkJlLzxxlCMAAEAAAH1MA0G
# CSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9u
# MRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp
# b24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI0
# MDcyNTE4MzEwMVoXDTI1MTAyMjE4MzEwMVowgdMxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9w
# ZXJhdGlvbnMgTGltaXRlZDEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjY1MUEt
# MDVFMC1EOTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNl
# MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAzO90cFQTWd/WP84IT7JM
# IW1fQL61sdfgmhlfT0nvYEb2kvkNF073ZwjveuSWot387LjE0TCiG93e6I0HzIFQ
# BnbxGP/WPBUirFq7WE5RAsuhNfYUL+PIb9jJq3CwWxICfw5t/pTyIOHjKvo1lQOT
# WZypir/psZwEE7y2uWAPbZJTFrKen5R73x2Hbxy4eW1DcmXjym2wFWv10sBH40aj
# Jfe+OkwcTdoYrY3KkpN/RQSjeycK0bhjo0CGYIYa+ZMAao0SNR/R1J1Y6sLkiCJO
# 3aQrbS1Sz7l+/qJgy8fyEZMND5Ms7C0sEaOvoBHiWSpTM4vc0xDLCmc6PGv03CtW
# u2KiyqrL8BAB1EYyOShI3IT79arDIDrL+de91FfjmSbBY5j+HvS0l3dXkjP3Hon8
# b74lWwikF0rzErF0n3khVAusx7Sm1oGG+06hz9XAy3Wou+T6Se6oa5LDiQgPTfWR
# /j9FNk8Ju06oSfTh6c03V0ulla0Iwy+HzUl+WmYxFLU0PiaXsmgudNwVqn51zr+B
# i3XPJ85wWuy6GGT7nBDmXNzTNkzK98DBQjTOabQXUZ884Yb9DFNcigmeVTYkyUXZ
# 6hscd8Nyq45A3D3bk+nXnsogK1Z7zZj6XbGft7xgOYvveU6p0+frthbF7MXv+i5q
# cD9HfFmOq4VYHevVesYb6P0CAwEAAaOCAUkwggFFMB0GA1UdDgQWBBRV4Hxb9Uo0
# oHDwJZJe22ixe2B1ATAfBgNVHSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBf
# BgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3Bz
# L2NybC9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmww
# bAYIKwYBBQUHAQEEYDBeMFwGCCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29m
# dC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0El
# MjAyMDEwKDEpLmNydDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUF
# BwMIMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAgEAcwxmVPaA9xHf
# fuom0TOSp2hspuf1G0cHW/KXHAuhnpW8/Svlq5j9aKI/8/G6fGIQMr0zlpau8jy8
# 3I4zclGdJjl5S02SxDlUKawtWvgf7ida06PgjeQM1eX4Lut4bbPfT0FEp77G76hh
# ysXxTJNHv5y+fwThUeiiclihZwqcZMpa46m+oV6igTU6I0EnneotMqFs0Q3zHgVV
# r4WXjnG2Bcnkip42edyg/9iXczqTBrEkvTz0UlltpFGaQnLzq+No8VEgq0UG7W1E
# LZGhmmxFmHABwTT6sPJFV68DfLoC0iB9Qbb9VZ8mvbTV5JtISBklTuVAlEkzXi9L
# IjNmx+kndBfKP8dxG/xbRXptQDQDaCsS6ogLkwLgH6zSs+ul9WmzI0F8zImbhnZh
# UziIHheFo4H+ZoojPYcgTK6/3bkSbOabmQFf95B8B6e5WqXbS5s9OdMdUlW1gTI1
# r5u+WAwH2KG7dxneoTbf/jYl3TUtP7AHpyck2c0nun/Q0Cycpa9QUH/Dy01k6tQo
# mNXGjivg2/BGcgZJ0Hw8C6KVelEJ31xLoE21m9+NEgSKCRoFE1Lkma31SyIaynbd
# YEb8sOlZynMdm8yPldDwuF54vJiEArjrcDNXe6BobZUiTWSKvv1DJadR1SUCO/Od
# 21GgU+hZqu+dKgjKAYdeTIvi9R2rtLYwggdxMIIFWaADAgECAhMzAAAAFcXna54C
# m0mZAAAAAAAVMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UE
# CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z
# b2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZp
# Y2F0ZSBBdXRob3JpdHkgMjAxMDAeFw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMy
# MjVaMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQH
# EwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNV
# BAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0B
# AQEFAAOCAg8AMIICCgKCAgEA5OGmTOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51
# yMo1V/YBf2xK4OK9uT4XYDP/XE/HZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY
# 6GB9alKDRLemjkZrBxTzxXb1hlDcwUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9
# cmmvHaus9ja+NSZk2pg7uhp7M62AW36MEBydUv626GIl3GoPz130/o5Tz9bshVZN
# 7928jaTjkY+yOSxRnOlwaQ3KNi1wjjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDua
# Rr3tpK56KTesy+uDRedGbsoy1cCGMFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74
# kpEeHT39IM9zfUGaRnXNxF803RKJ1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2
# K26oElHovwUDo9Fzpk03dJQcNIIP8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5
# TI4CvEJoLhDqhFFG4tG9ahhaYQFzymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZk
# i1ugpoMhXV8wdJGUlNi5UPkLiWHzNgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9Q
# BXpsxREdcu+N+VLEhReTwDwV2xo3xwgVGD94q0W29R6HXtqPnhZyacaue7e3Pmri
# Lq0CAwEAAaOCAd0wggHZMBIGCSsGAQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUC
# BBYEFCqnUv5kxJq+gpE8RjUpzxD/LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJl
# pxtTNRnpcjBcBgNVHSAEVTBTMFEGDCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIB
# FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9y
# eS5odG0wEwYDVR0lBAwwCgYIKwYBBQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUA
# YgBDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU
# 1fZWy4/oolxiaNE9lJBb186aGMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2Ny
# bC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIw
# MTAtMDYtMjMuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDov
# L3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0w
# Ni0yMy5jcnQwDQYJKoZIhvcNAQELBQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/yp
# b+pcFLY+TkdkeLEGk5c9MTO1OdfCcTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulm
# ZzpTTd2YurYeeNg2LpypglYAA7AFvonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM
# 9W0jVOR4U3UkV7ndn/OOPcbzaN9l9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECW
# OKz3+SmJw7wXsFSFQrP8DJ6LGYnn8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4
# FOmRsqlb30mjdAy87JGA0j3mSj5mO0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3Uw
# xTSwethQ/gpY3UA8x1RtnWN0SCyxTkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPX
# fx5bRAGOWhmRaw2fpCjcZxkoJLo4S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVX
# VAmxaQFEfnyhYWxz/gq77EFmPWn9y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGC
# onsXHRWJjXD+57XQKBqJC4822rpM+Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU
# 5nR0W2rRnj7tfqAxM328y+l7vzhwRNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEG
# ahC0HVUzWLOhcGbyoYIDWTCCAkECAQEwggEBoYHZpIHWMIHTMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl
# bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVT
# Tjo2NTFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAg
# U2VydmljZaIjCgEBMAcGBSsOAwIaAxUAJsAKu48NbR5YRg3WSBQCyjzdkvaggYMw
# gYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD
# VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsF
# AAIFAOrN+6QwIhgPMjAyNDEwMzExMjUwNDRaGA8yMDI0MTEwMTEyNTA0NFowdzA9
# BgorBgEEAYRZCgQBMS8wLTAKAgUA6s37pAIBADAKAgEAAgIBoAIB/zAHAgEAAgIT
# eTAKAgUA6s9NJAIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAow
# CAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBCwUAA4IBAQAM5jjlcu2n
# tYXJ7gNa2aj7nY7sT1BWMiqVLg1wHDdHZ4G0076KDHUv6u+dTLgWR/yFzczWRtL4
# 8yjgwW6lZtUH/V5KrRM4kL6NkQsOBKVk7Tfqiz/rN2sjakx9cmCezY/AVU2GXKSV
# 1WkQ6O7gYcwESeI54buhDq/gZZ//uvWPpN82Bkhbp4B6v8bxWn77ca95jElIEx0S
# OJO1wTBfvemZ2L/2+dVehIdc8E1/QyefWFlsBpfMuDy1oKGNZAfEMAvrc59FJbiv
# DcGbpZlHypnd4KJklNeChSkstoD7SJSxe5LqXWDnol4PO04vpyQEQspKJvyngMmB
# No4LxAVQPfYgMYIEDTCCBAkCAQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
# Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENB
# IDIwMTACEzMAAAH1mQmUvPHGUIwAAQAAAfUwDQYJYIZIAWUDBAIBBQCgggFKMBoG
# CSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQgbqxAEm4F
# u63pDZZaKY1j5JKFSy6ddL5F6XfB81TvaWwwgfoGCyqGSIb3DQEJEAIvMYHqMIHn
# MIHkMIG9BCDB1vLSFwh09ISu4kdEv4/tg9eR1Yk8w5x7j5GThqaPNTCBmDCBgKR+
# MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMT
# HU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAAB9ZkJlLzxxlCMAAEA
# AAH1MCIEIGEw3XPnzsoDHxxINJFN0yjYRotGutszxUwBAQrkKOjJMA0GCSqGSIb3
# DQEBCwUABIICAJsWXBVedTROPHU0y3z61gH/IhmRxCgy6Ane/WPVC/UmBT2MW0z2
# CktwrLZJ70mFWRvzVSLOoCvytSWzLljgxqIkvsjuLVWR71yK6NrrGVGi7GE4S5lq
# SApyHfH33qiqVi+jH6zVuT8lffTddLbo0UXKBwC/ZSkcGCF28fTmcLImjB+3cYgE
# as6P+kLed3L3X8CurttyVLu2F3tTS04sp5HqHnh781nM09Z+mCa9PvE/p+SsTb3Z
# 0LuaTQ+FqRB1fgvMqBjq/LTq1J92PqajjlJmk6Gc2Yj80y10T16BQI/iuNJWbUfC
# nSUtip1ZmA/XMOPHThVU5kOh4FPWRBRCW2izBqNSBDaxxcZUzFVgWr8VBx+LLFgh
# KVZcKNQ3DUYwbDjHjjFxHfDlxRqZnMLOg7s6D8kbgyAwNkL8z5cwVi+cG+l3EBRC
# EWCMn0J6Vqf+qjOq8e491vsdjZLfhRCPgcpID90ZV1feQmDCYIRdusfeoOSEJK44
# 5R/3WKFeAgvEUdu8Vc1DoXQkQG4fqWSQbngOTmY3FLTYKGbd4sAqMgjlBbK6kMxb
# Y/7k5q1KRi9K5E1KSrwue22tiFhO7w1EUXorpOFahxvbhNKi9BN7onetww5o66u+
# EFDXixWAPcfvbRfjUBWR0Y8cfMKLwQ5yY2y5MFP6lONp5fEI3orUDLoi
# SIG # End signature block