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) { 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." } try { try { $Command = 'nslookup ' + $ResultUrl.Host + ' -type=soa' $SSHRes = Invoke-SSHCommand -Command $Command -SSHSession $SSH_Sessions['VC'].Value } catch { throw "The FQDN $($ResultUrl.Host) cannot be resolved to an IP address. Make sure DNS is configured." } try { $Command = 'nc -vz ' + $ResultUrl.Host + ' ' + $ResultUrl.Port $SSHRes = Invoke-SSHCommand -Command $Command -SSHSession $SSH_Sessions['VC'].Value } 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 ("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 } } 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 = @() if ($PSBoundParameters.ContainsKey('SSLCertificatesSasUrl')) { $DestinationFileArray = Get-Certificates -SSLCertificatesSasUrl $SSLCertificatesSasUrl -ErrorAction Stop } else { $remoteComputers = , $PrimaryUrl if ($PSBoundParameters.ContainsKey('SecondaryUrl')) { $remoteComputers += $SecondaryUrl } $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 "Adding the LDAPS Identity Source..." 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 $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 } } # SIG # Begin signature block # MIInvwYJKoZIhvcNAQcCoIInsDCCJ6wCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCB0M2dmZSHfNakA # lFxdmdrssvGAD6c2E+uOOzeWmq9GRKCCDXYwggX0MIID3KADAgECAhMzAAADrzBA # DkyjTQVBAAAAAAOvMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjMxMTE2MTkwOTAwWhcNMjQxMTE0MTkwOTAwWjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQDOS8s1ra6f0YGtg0OhEaQa/t3Q+q1MEHhWJhqQVuO5amYXQpy8MDPNoJYk+FWA # hePP5LxwcSge5aen+f5Q6WNPd6EDxGzotvVpNi5ve0H97S3F7C/axDfKxyNh21MG # 0W8Sb0vxi/vorcLHOL9i+t2D6yvvDzLlEefUCbQV/zGCBjXGlYJcUj6RAzXyeNAN # xSpKXAGd7Fh+ocGHPPphcD9LQTOJgG7Y7aYztHqBLJiQQ4eAgZNU4ac6+8LnEGAL # go1ydC5BJEuJQjYKbNTy959HrKSu7LO3Ws0w8jw6pYdC1IMpdTkk2puTgY2PDNzB # tLM4evG7FYer3WX+8t1UMYNTAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQURxxxNPIEPGSO8kqz+bgCAQWGXsEw # RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW # MBQGA1UEBRMNMjMwMDEyKzUwMTgyNjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci # tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG # CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu # Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0 # MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAISxFt/zR2frTFPB45Yd # mhZpB2nNJoOoi+qlgcTlnO4QwlYN1w/vYwbDy/oFJolD5r6FMJd0RGcgEM8q9TgQ # 2OC7gQEmhweVJ7yuKJlQBH7P7Pg5RiqgV3cSonJ+OM4kFHbP3gPLiyzssSQdRuPY # 1mIWoGg9i7Y4ZC8ST7WhpSyc0pns2XsUe1XsIjaUcGu7zd7gg97eCUiLRdVklPmp # XobH9CEAWakRUGNICYN2AgjhRTC4j3KJfqMkU04R6Toyh4/Toswm1uoDcGr5laYn # TfcX3u5WnJqJLhuPe8Uj9kGAOcyo0O1mNwDa+LhFEzB6CB32+wfJMumfr6degvLT # e8x55urQLeTjimBQgS49BSUkhFN7ois3cZyNpnrMca5AZaC7pLI72vuqSsSlLalG # OcZmPHZGYJqZ0BacN274OZ80Q8B11iNokns9Od348bMb5Z4fihxaBWebl8kWEi2O # PvQImOAeq3nt7UWJBzJYLAGEpfasaA3ZQgIcEXdD+uwo6ymMzDY6UamFOfYqYWXk # ntxDGu7ngD2ugKUuccYKJJRiiz+LAUcj90BVcSHRLQop9N8zoALr/1sJuwPrVAtx # HNEgSW+AKBqIxYWM4Ev32l6agSUAezLMbq5f3d8x9qzT031jMDT+sUAoCw0M5wVt # CUQcqINPuYjbS1WgJyZIiEkBMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq # 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 # /Xmfwb1tbWrJUnMTDXpQzTGCGZ8wghmbAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw # EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN # aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp # Z25pbmcgUENBIDIwMTECEzMAAAOvMEAOTKNNBUEAAAAAA68wDQYJYIZIAWUDBAIB # BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO # MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIIzh3tU+2UNEy4MsA7JyHtIO # NFRRdM4o2PCakgUXAUJrMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A # cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB # BQAEggEAACnbej23o5gIKLTxpM6lmE5QGYwcmsHRjbvM3PC2wL7O4Gysj4YA5Oq4 # l6zGSd0WTwxAhXBILyf9qpzVTcpqJ17qbGcP0vMksLllfPXJTtAD1TzUuplEI/qc # 0o5XosYiSDD0tUpTrmtxk/3e5qPfjv2+8rnnfWOO1UF2ab9YnQhaYSkMy/02mCfh # jGdsX8YWDPDZ7XQ8gS5nCqJ+dN0YG/yEoFch80mF61hrlrBLFVRQ0eTWurgvtfsp # P6OMYXygAJCG0N9rdZyxWEmnTaVXO4tZmQ+laOwZ0o4MuINtxt0oW6wnCKMpS1ni # Q3hn9ebrfVCf871etd9X23jMY52/RaGCFykwghclBgorBgEEAYI3AwMBMYIXFTCC # FxEGCSqGSIb3DQEHAqCCFwIwghb+AgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFZBgsq # hkiG9w0BCRABBKCCAUgEggFEMIIBQAIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl # AwQCAQUABCB4S/OIe/LAIvVxpyvDQ/I2+z8VbAHD7eE4H3mxaA555wIGZfx6BGEU # GBMyMDI0MDQxMjE5NDQ1MS44MThaMASAAgH0oIHYpIHVMIHSMQswCQYDVQQGEwJV # UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl # bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNO # OjhENDEtNEJGNy1CM0I3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBT # ZXJ2aWNloIIReDCCBycwggUPoAMCAQICEzMAAAHj372bmhxogyIAAQAAAeMwDQYJ # KoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x # EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv # bjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwHhcNMjMx # MDEyMTkwNzI5WhcNMjUwMTEwMTkwNzI5WjCB0jELMAkGA1UEBhMCVVMxEzARBgNV # BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv # c29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxhbmQgT3Bl # cmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjo4RDQxLTRC # RjctQjNCNzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZTCC # AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL6kDWgeRp+fxSBUD6N/yuEJ # pXggzBeNG5KB8M9AbIWeEokJgOghlMg8JmqkNsB4Wl1NEXR7cL6vlPCsWGLMhyqm # scQu36/8h2bx6TU4M8dVZEd6V4U+l9gpte+VF91kOI35fOqJ6eQDMwSBQ5c9ElPF # UijTA7zV7Y5PRYrS4FL9p494TidCpBEH5N6AO5u8wNA/jKO94Zkfjgu7sLF8SUdr # c1GRNEk2F91L3pxR+32FsuQTZi8hqtrFpEORxbySgiQBP3cH7fPleN1NynhMRf6T # 7XC1L0PRyKy9MZ6TBWru2HeWivkxIue1nLQb/O/n0j2QVd42Zf0ArXB/Vq54gQ8J # IvUH0cbvyWM8PomhFi6q2F7he43jhrxyvn1Xi1pwHOVsbH26YxDKTWxl20hfQLdz # z4RVTo8cFRMdQCxlKkSnocPWqfV/4H5APSPXk0r8Cc/cMmva3g4EvupF4ErbSO0U # NnCRv7UDxlSGiwiGkmny53mqtAZ7NLePhFtwfxp6ATIojl8JXjr3+bnQWUCDCd5O # ap54fGeGYU8KxOohmz604BgT14e3sRWABpW+oXYSCyFQ3SZQ3/LNTVby9ENsuEh2 # UIQKWU7lv7chrBrHCDw0jM+WwOjYUS7YxMAhaSyOahpbudALvRUXpQhELFoO6tOx # /66hzqgjSTOEY3pu46BFAgMBAAGjggFJMIIBRTAdBgNVHQ4EFgQUsa4NZr41Fbeh # Z8Y+ep2m2YiYqQMwHwYDVR0jBBgwFoAUn6cVXQBeYl2D9OXSZacbUzUZ6XIwXwYD # VR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9j # cmwvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3JsMGwG # CCsGAQUFBwEBBGAwXjBcBggrBgEFBQcwAoZQaHR0cDovL3d3dy5taWNyb3NvZnQu # Y29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIw # MjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcD # CDAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQELBQADggIBALe+my6p1NPMEW1t # 70a8Y2hGxj6siDSulGAs4UxmkfzxMAic4j0+GTPbHxk193mQ0FRPa9dtbRbaezV0 # GLkEsUWTGF2tP6WsDdl5/lD4wUQ76ArFOencCpK5svE0sO0FyhrJHZxMLCOclvd6 # vAIPOkZAYihBH/RXcxzbiliOCr//3w7REnsLuOp/7vlXJAsGzmJesBP/0ERqxjKu # dPWuBGz/qdRlJtOl5nv9NZkyLig4D5hy9p2Ec1zaotiLiHnJ9mlsJEcUDhYj8PnY # nJjjsCxv+yJzao2aUHiIQzMbFq+M08c8uBEf+s37YbZQ7XAFxwe2EVJAUwpWjmtJ # 3b3zSWTMmFWunFr2aLk6vVeS0u1MyEfEv+0bDk+N3jmsCwbLkM9FaDi7q2HtUn3z # 6k7AnETc28dAvLf/ioqUrVYTwBrbRH4XVFEvaIQ+i7esDQicWW1dCDA/J3xOoCEC # V68611jriajfdVg8o0Wp+FCg5CAUtslgOFuiYULgcxnqzkmP2i58ZEa0rm4LZymH # BzsIMU0yMmuVmAkYxbdEDi5XqlZIupPpqmD6/fLjD4ub0SEEttOpg0np0ra/MNCf # v/tVhJtz5wgiEIKX+s4akawLfY+16xDB64Nm0HoGs/Gy823ulIm4GyrUcpNZxnXv # E6OZMjI/V1AgSAg8U/heMWuZTWVUMIIHcTCCBVmgAwIBAgITMwAAABXF52ueAptJ # mQAAAAAAFTANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT # Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m # dCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNh # dGUgQXV0aG9yaXR5IDIwMTAwHhcNMjEwOTMwMTgyMjI1WhcNMzAwOTMwMTgzMjI1 # WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH # UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD # Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCAiIwDQYJKoZIhvcNAQEB # BQADggIPADCCAgoCggIBAOThpkzntHIhC3miy9ckeb0O1YLT/e6cBwfSqWxOdcjK # NVf2AX9sSuDivbk+F2Az/1xPx2b3lVNxWuJ+Slr+uDZnhUYjDLWNE893MsAQGOhg # fWpSg0S3po5GawcU88V29YZQ3MFEyHFcUTE3oAo4bo3t1w/YJlN8OWECesSq/XJp # rx2rrPY2vjUmZNqYO7oaezOtgFt+jBAcnVL+tuhiJdxqD89d9P6OU8/W7IVWTe/d # vI2k45GPsjksUZzpcGkNyjYtcI4xyDUoveO0hyTD4MmPfrVUj9z6BVWYbWg7mka9 # 7aSueik3rMvrg0XnRm7KMtXAhjBcTyziYrLNueKNiOSWrAFKu75xqRdbZ2De+JKR # Hh09/SDPc31BmkZ1zcRfNN0Sidb9pSB9fvzZnkXftnIv231fgLrbqn427DZM9itu # qBJR6L8FA6PRc6ZNN3SUHDSCD/AQ8rdHGO2n6Jl8P0zbr17C89XYcz1DTsEzOUyO # ArxCaC4Q6oRRRuLRvWoYWmEBc8pnol7XKHYC4jMYctenIPDC+hIK12NvDMk2ZItb # oKaDIV1fMHSRlJTYuVD5C4lh8zYGNRiER9vcG9H9stQcxWv2XFJRXRLbJbqvUAV6 # bMURHXLvjflSxIUXk8A8FdsaN8cIFRg/eKtFtvUeh17aj54WcmnGrnu3tz5q4i6t # AgMBAAGjggHdMIIB2TASBgkrBgEEAYI3FQEEBQIDAQABMCMGCSsGAQQBgjcVAgQW # BBQqp1L+ZMSavoKRPEY1Kc8Q/y8E7jAdBgNVHQ4EFgQUn6cVXQBeYl2D9OXSZacb # UzUZ6XIwXAYDVR0gBFUwUzBRBgwrBgEEAYI3TIN9AQEwQTA/BggrBgEFBQcCARYz # aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnku # aHRtMBMGA1UdJQQMMAoGCCsGAQUFBwMIMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIA # QwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNX2 # VsuP6KJcYmjRPZSQW9fOmhjEMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwu # bWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEw # LTA2LTIzLmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93 # d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYt # MjMuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCdVX38Kq3hLB9nATEkW+Geckv8qW/q # XBS2Pk5HZHixBpOXPTEztTnXwnE2P9pkbHzQdTltuw8x5MKP+2zRoZQYIu7pZmc6 # U03dmLq2HnjYNi6cqYJWAAOwBb6J6Gngugnue99qb74py27YP0h1AdkY3m2CDPVt # I1TkeFN1JFe53Z/zjj3G82jfZfakVqr3lbYoVSfQJL1AoL8ZthISEV09J+BAljis # 9/kpicO8F7BUhUKz/AyeixmJ5/ALaoHCgRlCGVJ1ijbCHcNhcy4sa3tuPywJeBTp # kbKpW99Jo3QMvOyRgNI95ko+ZjtPu4b6MhrZlvSP9pEB9s7GdP32THJvEKt1MMU0 # sHrYUP4KWN1APMdUbZ1jdEgssU5HLcEUBHG/ZPkkvnNtyo4JvbMBV0lUZNlz138e # W0QBjloZkWsNn6Qo3GcZKCS6OEuabvshVGtqRRFHqfG3rsjoiV5PndLQTHa1V1QJ # sWkBRH58oWFsc/4Ku+xBZj1p/cvBQUl+fpO+y/g75LcVv7TOPqUxUYS8vwLBgqJ7 # Fx0ViY1w/ue10CgaiQuPNtq6TPmb/wrpNPgkNWcr4A245oyZ1uEi6vAnQj0llOZ0 # dFtq0Z4+7X6gMTN9vMvpe784cETRkPHIqzqKOghif9lwY1NNje6CbaUFEMFxBmoQ # tB1VM1izoXBm8qGCAtQwggI9AgEBMIIBAKGB2KSB1TCB0jELMAkGA1UEBhMCVVMx # EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT # FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxh # bmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjo4 # RDQxLTRCRjctQjNCNzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vy # dmljZaIjCgEBMAcGBSsOAwIaAxUAPYiXu8ORQ4hvKcuE7GK0COgxWnqggYMwgYCk # fjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH # UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD # Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIF # AOnD91QwIhgPMjAyNDA0MTMwMjA5MjRaGA8yMDI0MDQxNDAyMDkyNFowdDA6Bgor # BgEEAYRZCgQBMSwwKjAKAgUA6cP3VAIBADAHAgEAAgITYzAHAgEAAgISYDAKAgUA # 6cVI1AIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAID # B6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBAE/Zqh5mt77s2LJe7nSU # CB/Ts9B2wAUZPi8z/4n20FdNofF1g9CNYwCVOhJ5LlG58CG5TCbKP9FLH6zb4zj0 # NfFkLAw0UcvyUrsKz212BZrhpy2Ky1gXGWWxNd1qmcWSPC4u/qmqAUVm60m2DIPR # pmcyNhvcBEtHndSNdnwMctlQMYIEDTCCBAkCAQEwgZMwfDELMAkGA1UEBhMCVVMx # EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT # FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUt # U3RhbXAgUENBIDIwMTACEzMAAAHj372bmhxogyIAAQAAAeMwDQYJYIZIAWUDBAIB # BQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQx # IgQgNr1+wsoCJXoVMPNaSeMjvLHpCgzp7TME5MDHSRqTSIYwgfoGCyqGSIb3DQEJ # EAIvMYHqMIHnMIHkMIG9BCAz1COr5bD+ZPdEgQjWvcIWuDJcQbdgq8Ndj0xyMuYm # KjCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw # DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x # JjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAAB49+9 # m5ocaIMiAAEAAAHjMCIEIAu5UEUU8gDbWLmm8x5l6LZ3bq0yFnv1pbteY8y1zUmg # MA0GCSqGSIb3DQEBCwUABIICAHe4BtZeCtH1tyUK2Ajt1w3knKj1fpcs9pfNWHoz # 3iEIlCDgwle/lwR0cOQlM/qIEslgnZ6/Mj8miM1echzl2wk33OjUeL2Lj7tIiM0/ # UOYZMcqI7XSuY/eUUpy4+c8E20XkeQcQFW9bQ8iX2JYkozLePAdvZk2evAOSWjFN # rZUz7M+VgwVKp3BnPP38WZWPcGAaZwFm4v4gD8L2Iyu44Tk2s3iWGLU3LJ5e3aJD # 6lErDwd++Ig+Jgwqt5Ad8vA2RajZtbGI9eK+u/75ech83WMEfQWXHkK7EPzCX+Ts # YZVHyIweO8bLwgNSU0PLhcr04tajU0JE5TNVl0fbncu3TX6X0uSv1sxem20d0MvE # mVSjtmuMtQFMI9woP5qLxwH052Eki8ZvR0QBqmew4DQfx5RRF0TBZztY2DCWs7fd # knkiWghGkChWhSiKVWWVPNtNYiYfrpfVzdIT+LxW+41L5rOobhcchM9mv0s1VmOz # f/DyafMTzo1ZTpiaoaYl1iEK2JhNjXXjRNX0xxxFcQAyWgBjR+xwhEjg3KOVx6mN # SNZkifuQMTRV46H2baL5APPzUjmIGpAQ3ZJON8pMdLstCA6VoW12BiZ167C66x9O # ktbhi4k3bEJyVCzJDJrNjU7PIrg0aBgbAtyhlyUvNKEpAk2+Y/YujblgvmAsmhJI # v/dM # SIG # End signature block |