Microsoft.AVS.Management.psm1
#Requires -Modules PowerShellGet #Requires -Version 5.0 <# AVSAttribute applied to a commandlet function indicates: - whether the SDDC should be marked as Building while the function executes. - default timeout for the commandlet, maximum: 3h. AVS SDDC in Building state prevents other changes from being made to the SDDC until the function completes/fails. #> class AVSAttribute : Attribute { [bool]$UpdatesSDDC = $false [TimeSpan]$Timeout AVSAttribute($timeoutMinutes) { $this.Timeout = New-TimeSpan -Minutes $timeoutMinutes } } <# ======================================================================================================= AUTHOR: David Becher DATE: 4/22/2021 Version: 1.0.0 Comment: Cmdlets for various administrative functions of Azure VMWare Solution products Callouts: This script will require the powershell session running it to be able to authenticate to azure to pull secrets from key vault, will need service principal? Also make sure we don't allow code injections ======================================================================================================== #> <# 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) { Write-Error "If adding an LDAPS identity source, please ensure you pass in at least one certificate" -ErrorAction Stop } if ($PSBoundParameters.ContainsKey('SecondaryUrl') -and $CertificatesSASList.count -lt 2) { Write-Error "If passing in a secondary/fallback URL, ensure that at least two certificates are passed." -ErrorAction Stop } $DestinationFileArray = @() $Index = 1 foreach ($CertSas in $CertificatesSASList) { Write-Host "Downloading Cert $Index..." $CertDir = $pwd.Path $CertLocation = "$CertDir/cert$Index.cer" $Index = $Index + 1 try { $Response = Invoke-WebRequest -Uri $CertSas -OutFile $CertLocation $StatusCode = $Response.StatusCode Write-Host("Certificate downloaded. $StatusCode") $DestinationFileArray += $CertLocation } catch { Write-Error "Ensure the SAS string is still valid" -ErrorAction Continue Write-Error $PSItem.Exception.Message -ErrorAction Continue Write-Error "Failed to download certificate ($Index-1)" -ErrorAction Stop } } 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 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 = $true, HelpMessage = '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 = Get-Certificates -SSLCertificatesSasUrl $SSLCertificatesSasUrl -ErrorAction Stop [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 = $true, HelpMessage = '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) { $DestinationFileArray = Get-Certificates $SSLCertificatesSasUrl -ErrorAction Stop [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 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 } 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. It may already have been added. 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 $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 } } } } Export-ModuleMember -Function * # SIG # Begin signature block # MIIn+AYJKoZIhvcNAQcCoIIn6TCCJ+UCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAJdrk7L3gN8WMW # vaEwL2h9CTuBQ9YXAquORGI7dO9OW6CCDYEwggX/MIID56ADAgECAhMzAAACUosz # qviV8znbAAAAAAJSMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjEwOTAyMTgzMjU5WhcNMjIwOTAxMTgzMjU5WjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQDQ5M+Ps/X7BNuv5B/0I6uoDwj0NJOo1KrVQqO7ggRXccklyTrWL4xMShjIou2I # sbYnF67wXzVAq5Om4oe+LfzSDOzjcb6ms00gBo0OQaqwQ1BijyJ7NvDf80I1fW9O # L76Kt0Wpc2zrGhzcHdb7upPrvxvSNNUvxK3sgw7YTt31410vpEp8yfBEl/hd8ZzA # v47DCgJ5j1zm295s1RVZHNp6MoiQFVOECm4AwK2l28i+YER1JO4IplTH44uvzX9o # RnJHaMvWzZEpozPy4jNO2DDqbcNs4zh7AWMhE1PWFVA+CHI/En5nASvCvLmuR/t8 # q4bc8XR8QIZJQSp+2U6m2ldNAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUNZJaEUGL2Guwt7ZOAu4efEYXedEw # UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1 # ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDY3NTk3MB8GA1UdIwQYMBaAFEhu # ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu # bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w # Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3 # Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx # MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAFkk3 # uSxkTEBh1NtAl7BivIEsAWdgX1qZ+EdZMYbQKasY6IhSLXRMxF1B3OKdR9K/kccp # kvNcGl8D7YyYS4mhCUMBR+VLrg3f8PUj38A9V5aiY2/Jok7WZFOAmjPRNNGnyeg7 # l0lTiThFqE+2aOs6+heegqAdelGgNJKRHLWRuhGKuLIw5lkgx9Ky+QvZrn/Ddi8u # TIgWKp+MGG8xY6PBvvjgt9jQShlnPrZ3UY8Bvwy6rynhXBaV0V0TTL0gEx7eh/K1 # o8Miaru6s/7FyqOLeUS4vTHh9TgBL5DtxCYurXbSBVtL1Fj44+Od/6cmC9mmvrti # yG709Y3Rd3YdJj2f3GJq7Y7KdWq0QYhatKhBeg4fxjhg0yut2g6aM1mxjNPrE48z # 6HWCNGu9gMK5ZudldRw4a45Z06Aoktof0CqOyTErvq0YjoE4Xpa0+87T/PVUXNqf # 7Y+qSU7+9LtLQuMYR4w3cSPjuNusvLf9gBnch5RqM7kaDtYWDgLyB42EfsxeMqwK # WwA+TVi0HrWRqfSx2olbE56hJcEkMjOSKz3sRuupFCX3UroyYf52L+2iVTrda8XW # esPG62Mnn3T8AuLfzeJFuAbfOSERx7IFZO92UPoXE1uEjL5skl1yTZB3MubgOA4F # 8KoRNhviFAEST+nG8c8uIsbZeb08SeYQMqjVEmkwggd6MIIFYqADAgECAgphDpDS # AAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMK # V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0 # IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0 # ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5MDlaFw0yNjA3MDgyMTA5MDla # MH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS # ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMT # H01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTEwggIiMA0GCSqGSIb3DQEB # AQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQTTS68rZYIZ9CGypr6VpQqrgG # OBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULTiQ15ZId+lGAkbK+eSZzpaF7S # 35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYSL+erCFDPs0S3XdjELgN1q2jz # y23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494HDdVceaVJKecNvqATd76UPe/7 # 4ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZPrGMXeiJT4Qa8qEvWeSQOy2u # M1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5bmR/U7qcD60ZI4TL9LoDho33 # X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGSrhwjp6lm7GEfauEoSZ1fiOIl # XdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADhvKwCgl/bwBWzvRvUVUvnOaEP # 6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON7E1JMKerjt/sW5+v/N2wZuLB # l4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xcv3coKPHtbcMojyyPQDdPweGF # RInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqwiBfenk70lrC8RqBsmNLg1oiM # CwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFEhuZOVQ # BdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1Ud # DwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFHItOgIxkEO5FAVO # 4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwubWljcm9zb2Z0 # LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y # Mi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUFBzAChkJodHRwOi8vd3d3Lm1p # Y3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y # Mi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGCNy4DMIGDMD8GCCsGAQUFBwIB # FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2RvY3MvcHJpbWFyeWNw # cy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AcABvAGwAaQBjAHkA # XwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAGfyhqWY # 4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4sPvjDctFtg/6+P+gKyju/R6mj # 82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKLUtCw/WvjPgcuKZvmPRul1LUd # d5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7pKkFDJvtaPpoLpWgKj8qa1hJ # Yx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft0N3zDq+ZKJeYTQ49C/IIidYf # wzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4MnEnGn+x9Cf43iw6IGmYslmJ # aG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxvFX1Fp3blQCplo8NdUmKGwx1j # NpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG0QaxdR8UvmFhtfDcxhsEvt9B # xw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf0AApxbGbpT9Fdx41xtKiop96 # eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkYS//WsyNodeav+vyL6wuA6mk7 # r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrvQQqxP/uozKRdwaGIm1dxVk5I # RcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIZzTCCGckCAQEwgZUwfjELMAkG # A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx # HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z # b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAlKLM6r4lfM52wAAAAACUjAN # BglghkgBZQMEAgEFAKCB7zAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor # BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQg0oRvPcjx # srXcyTSu/GOZHJRSLetnku7Z+Nc0WSMaaZcwgYIGCisGAQQBgjcCAQwxdDByoDSA # MgBBAFYAUwAtAEEAdQB0AG8AbQBhAHQAaQBvAG4ALQBBAGQAbQBpAG4AVABvAG8A # bABzoTqAOGh0dHBzOi8vZ2l0aHViLmNvbS9BenVyZS9henVyZS1hdnMtYXV0b21h # dGlvbi1hZG1pbnRvb2xzMA0GCSqGSIb3DQEBAQUABIIBABe+eu0qCj7P/97xd7Fz # 2DemJyBLbKDFgi8flQJ4SHLvx+VxHRO7PO9DTXssk4G0No05JGz6puuBjB7yibEj # tTagWe/KkFQxQRxOc4OU4VV9USESnDdYKhqV7Wi4SI+zaxHavXiBYp0PHziPs0XL # dH8KTmmaycimc7XLizepBbt5talFW2Lz0sbhEGcCz5p/5sR70RkvCra6009ZDJXJ # KT8zECbbaEifxfg1/pgsjuDirZfuT3IryroA+3ZBmeM9PlDJXV0nPX50ZAXFmgn4 # LbrSv+tfgEbkREWsbYqLA+ocDRexXHBr87FyMphdm5tRbKMBH9G8wg+uwtMBH8M0 # rVihghcWMIIXEgYKKwYBBAGCNwMDATGCFwIwghb+BgkqhkiG9w0BBwKgghbvMIIW # 6wIBAzEPMA0GCWCGSAFlAwQCAQUAMIIBWQYLKoZIhvcNAQkQAQSgggFIBIIBRDCC # AUACAQEGCisGAQQBhFkKAwEwMTANBglghkgBZQMEAgEFAAQgc7YQRNp3EtoSwFgs # kYDnx5o0FKCecD7s4nQB98u4o/wCBmJsSiuv2xgTMjAyMjA1MDYxOTM3MDIuNjY3 # WjAEgAIB9KCB2KSB1TCB0jELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0 # b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3Jh # dGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxhbmQgT3BlcmF0aW9ucyBMaW1p # dGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjozQkQ0LTRCODAtNjlDMzElMCMG # A1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaCCEWUwggcUMIIE/KAD # AgECAhMzAAABibS/hjCEHEuPAAEAAAGJMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNV # BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4w # HAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29m # dCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTIxMTAyODE5Mjc0MVoXDTIzMDEyNjE5 # Mjc0MVowgdIxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYD # VQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLTAr # BgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEmMCQG # A1UECxMdVGhhbGVzIFRTUyBFU046M0JENC00QjgwLTY5QzMxJTAjBgNVBAMTHE1p # Y3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEBAQUAA4IC # DwAwggIKAoICAQC9BlfFkWZrqmWa47K82lXzE407BxiiVkb8GPJlYZKTkk4ZovKs # oh3lXFUdYeWyYkThK+fOx2mwqZXHyi04294hQW9Jx4RmnxVea7mbV+7wvtz7eXBd # yJuNxyq0S+1CyWiRBXHSv4vnhpus0NqvAUbvchpGJ0hLWL1z66cnyhjKENEusLKw # UBXHJCE81mRYrtnz9Ua6RoosBYdcKH/5HneHjaAUv73+YAAvHMJde6h+Lx/9coKb # vE3BVzWE40ILPqir3gC5/NU2SQhbhutRCBikJwmb1TRc2ZC+2uilgOf1S1jxhDQ0 # p6dc+12Asd1Dw2e/eKASsoutYjRrmfmON0p/CT7ya9qSp1maU6x545LVeylA0kAr # W5mWUAhNydBk5w7mh+M5Dfe6NZyQBd3P7/HejuXgBT9NI4zMZkzCFR21XALd1Jsi # 2lJUWCeMzYI4Qn3OAJp286KsYMs3jvWNkjaMKWSOwlN2A+TfjdNADgkW92z+6dmr # S4uv6eJndfjg4HHbH6BWWWfZzhRtlc254DjJLVMkZtskUggsCZNQD0C6Pl4hIZNs # 2LJbHv0ecI5Nqvf1AQqjObgudOYNfLT8oj8f+dhkYq5Md9yQ/bzBBLTqsP58NLnE # vBxEwJb3YOQdea1uEbJGKUE4vkvFl6VB/G3njCXhZQLQB0ASiU96Q4PA7wIDAQAB # o4IBNjCCATIwHQYDVR0OBBYEFJdvH7NHWngggB6C4DqscqSt+XtQMB8GA1UdIwQY # MBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCGTmh0dHA6 # Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUyMFRpbWUt # U3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4wXAYIKwYB # BQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvTWlj # cm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwGA1UdEwEB # /wQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwgwDQYJKoZIhvcNAQELBQADggIBAI60 # t2lZQjgrB8sut9oqssH3YOpsCykZYzjVNo7gmX6wfE+jnba67cYpAKOaRFat4e2V # /LL2Q6TstZrHeTeR7wa19619uHuofQt5XZc5aDf0E6cd/qZNxmrsVhJllyHUkNCN # z3z452WjD6haKHQNu3gJX97X1lwT7WfXPNaSyRQR3R/mM8hSKzfen6+RjyzN24C0 # Jwhw8VSEjwdvlqU9QA8yMbPApvs0gpud/yPxw/XwCzki95yQXSiHVzDrdFj+88rr # YsNh2mLtacbY5u+eB9ZUq3CLBMjiMePZw72rfscN788+XbXqBKlRmHRqnbiYqYwN # 9wqnU3iYR2zHPiix46s9h4WwcdYkUnoCK++qfvQpN4mmnmv4PFKpt5LLSbEhQ6r+ # UBpTGA1JBVRfbq3yv59yKSh8q/bdYeu1FXe3utVOwH1jOtFqKKSbPrwrkdZ230yp # QvE9A+j6mlnQtGqQ5p7jrr5QpFjQnFa12sxzm8eUdl+eqNrCP9GwzZLpDp9r1P0K # djU3PsNgEbfJknII8WyuBTTmz2WOp+xKm2kV1SH1Hhx74vvVJYMszbH/UwUsscAx # tewSnwqWgQa1oNQufG19La1iF+4oapFegR8M8Aych1O9A+HcYdDhKOSQEBEcvQxj # vlqWEZModaMLZotU6jyhsogGTyF+cUNR/8TJXDi5MIIHcTCCBVmgAwIBAgITMwAA # ABXF52ueAptJmQAAAAAAFTANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMx # EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT # FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3Qg # Q2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMjEwOTMwMTgyMjI1WhcNMzAw # OTMwMTgzMjI1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQ # MA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u # MSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCAiIwDQYJ # KoZIhvcNAQEBBQADggIPADCCAgoCggIBAOThpkzntHIhC3miy9ckeb0O1YLT/e6c # BwfSqWxOdcjKNVf2AX9sSuDivbk+F2Az/1xPx2b3lVNxWuJ+Slr+uDZnhUYjDLWN # E893MsAQGOhgfWpSg0S3po5GawcU88V29YZQ3MFEyHFcUTE3oAo4bo3t1w/YJlN8 # OWECesSq/XJprx2rrPY2vjUmZNqYO7oaezOtgFt+jBAcnVL+tuhiJdxqD89d9P6O # U8/W7IVWTe/dvI2k45GPsjksUZzpcGkNyjYtcI4xyDUoveO0hyTD4MmPfrVUj9z6 # BVWYbWg7mka97aSueik3rMvrg0XnRm7KMtXAhjBcTyziYrLNueKNiOSWrAFKu75x # qRdbZ2De+JKRHh09/SDPc31BmkZ1zcRfNN0Sidb9pSB9fvzZnkXftnIv231fgLrb # qn427DZM9ituqBJR6L8FA6PRc6ZNN3SUHDSCD/AQ8rdHGO2n6Jl8P0zbr17C89XY # cz1DTsEzOUyOArxCaC4Q6oRRRuLRvWoYWmEBc8pnol7XKHYC4jMYctenIPDC+hIK # 12NvDMk2ZItboKaDIV1fMHSRlJTYuVD5C4lh8zYGNRiER9vcG9H9stQcxWv2XFJR # XRLbJbqvUAV6bMURHXLvjflSxIUXk8A8FdsaN8cIFRg/eKtFtvUeh17aj54WcmnG # rnu3tz5q4i6tAgMBAAGjggHdMIIB2TASBgkrBgEEAYI3FQEEBQIDAQABMCMGCSsG # AQQBgjcVAgQWBBQqp1L+ZMSavoKRPEY1Kc8Q/y8E7jAdBgNVHQ4EFgQUn6cVXQBe # Yl2D9OXSZacbUzUZ6XIwXAYDVR0gBFUwUzBRBgwrBgEEAYI3TIN9AQEwQTA/Bggr # BgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1Jl # cG9zaXRvcnkuaHRtMBMGA1UdJQQMMAoGCCsGAQUFBwMIMBkGCSsGAQQBgjcUAgQM # HgoAUwB1AGIAQwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1Ud # IwQYMBaAFNX2VsuP6KJcYmjRPZSQW9fOmhjEMFYGA1UdHwRPME0wS6BJoEeGRWh0 # dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0Nl # ckF1dF8yMDEwLTA2LTIzLmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKG # Pmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0 # XzIwMTAtMDYtMjMuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCdVX38Kq3hLB9nATEk # W+Geckv8qW/qXBS2Pk5HZHixBpOXPTEztTnXwnE2P9pkbHzQdTltuw8x5MKP+2zR # oZQYIu7pZmc6U03dmLq2HnjYNi6cqYJWAAOwBb6J6Gngugnue99qb74py27YP0h1 # AdkY3m2CDPVtI1TkeFN1JFe53Z/zjj3G82jfZfakVqr3lbYoVSfQJL1AoL8ZthIS # EV09J+BAljis9/kpicO8F7BUhUKz/AyeixmJ5/ALaoHCgRlCGVJ1ijbCHcNhcy4s # a3tuPywJeBTpkbKpW99Jo3QMvOyRgNI95ko+ZjtPu4b6MhrZlvSP9pEB9s7GdP32 # THJvEKt1MMU0sHrYUP4KWN1APMdUbZ1jdEgssU5HLcEUBHG/ZPkkvnNtyo4JvbMB # V0lUZNlz138eW0QBjloZkWsNn6Qo3GcZKCS6OEuabvshVGtqRRFHqfG3rsjoiV5P # ndLQTHa1V1QJsWkBRH58oWFsc/4Ku+xBZj1p/cvBQUl+fpO+y/g75LcVv7TOPqUx # UYS8vwLBgqJ7Fx0ViY1w/ue10CgaiQuPNtq6TPmb/wrpNPgkNWcr4A245oyZ1uEi # 6vAnQj0llOZ0dFtq0Z4+7X6gMTN9vMvpe784cETRkPHIqzqKOghif9lwY1NNje6C # baUFEMFxBmoQtB1VM1izoXBm8qGCAtQwggI9AgEBMIIBAKGB2KSB1TCB0jELMAkG # A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx # HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9z # b2Z0IElyZWxhbmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMg # VFNTIEVTTjozQkQ0LTRCODAtNjlDMzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUt # U3RhbXAgU2VydmljZaIjCgEBMAcGBSsOAwIaAxUAIaUJreR63J657Ltsk2laQy6I # JxCggYMwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQ # MA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u # MSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG # 9w0BAQUFAAIFAOYfWh8wIhgPMjAyMjA1MDYxNjI2MDdaGA8yMDIyMDUwNzE2MjYw # N1owdDA6BgorBgEEAYRZCgQBMSwwKjAKAgUA5h9aHwIBADAHAgEAAgIJ5DAHAgEA # AgIRhjAKAgUA5iCrnwIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMC # oAowCAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBAJFvZ6Lj # M2me5H35TL9meaOSeMSgGxpTI+03PfiCDbYRjSs2TVBauZE1WD/TI6xMXZ7LSUcG # Wo+/3o8KNQQILHgmp7drBqbbxE8cKwGroAKTGnR6bJFJ/ZG+FsPtR/7jLcWC6UFg # XPLsVkYvg5psB9Q1GTSwb9j3jY0FRlb45pdVMYIEDTCCBAkCAQEwgZMwfDELMAkG # A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx # HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9z # b2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAGJtL+GMIQcS48AAQAAAYkwDQYJ # YIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkq # hkiG9w0BCQQxIgQgMZxrZ7qgysCrAg503/yLSCIaK/IE9ehdV8GCpIMo/CowgfoG # CyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCBmd0cx3FBXVWxulYc5MepYTJy9xEmb # txjr2X9SZPyPRTCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo # aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y # cG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEw # AhMzAAABibS/hjCEHEuPAAEAAAGJMCIEIGclonu2e0Y7cpCVwCaSYRCc3mqA4XLt # OnZKoH96jwotMA0GCSqGSIb3DQEBCwUABIICAGNXmc1OGibPzSooUBqtH7HzQuiV # s2Q5i4PcrGokapcZ+pwjIwl/3Tx8nrvDLGkut3GDBYuueFm7z9p6FQaFGaWU/zi2 # 3DnTqUHb1X1xX6OGM3yNhKrfiNw9V20m6gN8owMFIA8qRvR/8zUW6WZs/LmPeYwn # pJhougY91Sj4ZBd+PZpgGGD+24QR8iptObqQzoA9OXg8LgNsAjHcvsRys7jvsRXx # 0JkaMf7LE5jW42Mwoclsie5aK94DxnC0UpIPRHSzWK5AiiRJhu026eDE2I4yRPrJ # EiPPtxECNfTK4o5riZTW7VPsY0yQafuzaSXLWUytRi8kTvuLRQPoseIGRVpkzP94 # TcNs6ABScYIPjpocO4w8NNw5pG+Wdgx30cdMHjaO4ReMv5Owhi3rz7Nt4ewBHT4P # YoCUANPIKCo7xV/YkKJgIKMijGvl8BTOii89o6HzU1/9ZhxczLYHvrUxfqpM7pgw # FlxQS/slFRQE7GrFEMA+ryyM6f+VCHVzWJdc/6hjwyItR+mx5TkViSWuFWd9hO3P # qI4DZMSoHko+6CEzZGua3xZvbcf103s9eDAuba/EHd81+bgDXu1KJfqrw6VzylFP # ViRtP3eYa3tVCf5L6jBccC99S81Bx2JXR94sSI85TjHLhaMccMHJrejP51OTCSnb # yhrTfhml5Sj1TvYR # SIG # End signature block |