PureStorage.CommonUtil.ps1

<#
Common Utility functions.
#>


$DEFAULT_PER_HOST_TIMEOUT_IN_SECONDS = 30
$MIN_TIMEOUT_IN_MINUTES = 10
$MAX_TIMEOUT_IN_MINUTES = 60

function Get-PfaHostFromVmHost {
    <#
    .SYNOPSIS
      Gets a FlashArray host object from a ESXi vmhost object
    .DESCRIPTION
      Takes in a vmhost and returns a matching FA host if found
    #>


    Param(
        [Parameter(Mandatory=$true,ValueFromPipeline=$True)]
        [VMware.VimAutomation.ViCore.Types.V1.Inventory.VMHost]$Esxi,

        [Parameter(Mandatory=$true,ValueFromPipeline=$True)]
        $Flasharray
    )
    $iscsiadapter = $esxi | Get-VMHostHBA -Type iscsi | Where-Object {$_.Model -eq "iSCSI Software Adapter"}
    $fahosts = Get-Pfa2Host -Array $FlashArray | Where-Object {$_.IsLocal -eq $True}
    $ArrayName = Get-ArrayName -FlashArray $FlashArray
    if ($null -ne $iscsiadapter)
    {
        $iqn = $iscsiadapter.ExtensionData.IScsiName
        foreach ($fahost in $fahosts)
        {
            if ($fahost.iqns.count -ge 1)
            {
                foreach ($fahostiqn in $fahost.iqns)
                {
                    if ($iqn.ToLower() -eq $fahostiqn.ToLower())
                    {
                        $faHostMatch = $fahost
                        break
                    }
                }
            }
        }
    }
    if ($null -ne $faHostMatch)
    {
      return $faHostMatch
    }
    else
    {
        throw "No matching host for $($esxi.Name) could be found on the Pure Cloud Block Store $ArrayName"
    }
}

function Get-PfaHostGroupfromVcCluster {
    <#
    .SYNOPSIS
      Retrieves a FA host group from an ESXi cluster
    .DESCRIPTION
      Takes in a vCenter Cluster and retrieves corresonding host group
    #>


    Param(
        [Parameter(Mandatory=$true,ValueFromPipeline=$True)]
        [VMware.VimAutomation.ViCore.Types.V1.Inventory.Cluster]$Cluster,

        [Parameter(Mandatory=$True)]
        $Flasharray
    )

    $esxiHosts = $cluster |Get-VMHost
    $faHostGroups = @()
    $faHostGroupNames = @()
    $ArrayName = Get-ArrayName -FlashArray $FlashArray
    foreach ($esxiHost in $esxiHosts)
    {
        try {
            $faHost = $esxiHost | Get-PfaHostFromVmHost -flasharray $flasharray
            if ($null -ne $faHost.HostGroup.Name)
            {
                if ($faHostGroupNames.contains($faHost.HostGroup.Name))
                {
                    continue
                }
                else {
                    $faHostGroupNames += $faHost.HostGroup.Name
                    $faHostGroup = Get-Pfa2HostGroup -Array $Flasharray -Name $($faHost.HostGroup.Name)  -ErrorAction stop | Where-Object {$_.IsLocal -eq $True}
                    $faHostGroups += $faHostGroup
                }
            }
        }
        catch{
            continue
        }
    }
    if ($null -eq $faHostGroup)
    {
        throw "No host group found for cluster $($Cluster.Name) on $ArrayName."
    }
    if ($faHostGroups.count -gt 1)
    {
        Write-Warning -Message "Cluster $($Cluster.Name) spans more than one host group. The recommendation is to have only one host group per cluster"
    }
    return $faHostGroups
}


function Get-ArrayName {
    Param(
        [Parameter(Mandatory=$true)]
        $FlashArray
    )
    $ArrayName = (Get-Pfa2Array -Array $Flasharray).Name
    return $ArrayName
}

function Get-AzureAuthHeader {
    param (
        $AzContext
    )
    $AzProfile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile
    $ProfileClient = New-Object -TypeName Microsoft.Azure.Commands.ResourceManager.Common.RMProfileClient -ArgumentList ($AzProfile)
    $Token = $profileClient.AcquireAccessToken($azContext.Subscription.TenantId)
    if (-not $Token) {
        throw "Failed to acquire Azure access token for tenant $($azContext.Subscription.TenantId)"
    }
    $AuthHeader = @{
        'Content-Type'='application/json'
        'Authorization'='Bearer ' + $token.AccessToken
    }

    return $AuthHeader
}

function Get-AVSvCenterEndpoint {
    param (
        [Hashtable] $AuthHeader,
        [String] $SubscriptionId,
        [String] $AvsResourceGroupName,
        [String] $AvsPrivateCloudName
    )
    $RestUri = "https://management.azure.com/subscriptions/$($subscriptionId)/resourceGroups/$($avsResourceGroupName)/providers/Microsoft.AVS/privateClouds/$($avsPrivateCloudName)?api-version=2022-05-01"
    $Response = Invoke-RestMethod -Uri $restUri -Method Get -Headers $authHeader
    if (-not $Response) {
        throw "Failed to get AVS vCenter endpoint. Please make sure you have the right permission to access the AVS instance with subscriptionId $subscriptionId, resource group $avsResourceGroupName and private cloud $avsPrivateCloudName"
    }
    $VcsaEndpoint = $response.properties.endpoints.vcsa
    $VcsaIPAddress = [System.Uri]::new($vcsaEndpoint).Host

    return $vcsaIPAddress
}

function Get-AVSvCenterCredential {
    param (
        [Hashtable] $AuthHeader,
        [String] $SubscriptionId,
        [String] $AvsResourceGroupName,
        [String] $AvsPrivateCloudName
    )
    $RestUri = "https://management.azure.com/subscriptions/$($subscriptionId)/resourceGroups/$($avsResourceGroupName)/providers/Microsoft.AVS/privateClouds/$($avsPrivateCloudName)/listAdminCredentials?api-version=2022-05-01"
    $Response = Invoke-RestMethod -Uri $restUri -Method Post -Headers $AuthHeader
    if (-not $Response) {
        throw "Failed to get AVS vCenter credential. Please make sure you have the right permission to access the AVS instance with subscriptionId $subscriptionId, resource group $avsResourceGroupName and private cloud $avsPrivateCloudName"
    }
    return $Response
}

function Connect-AVSvCenter {
    param (
        [String] $AVSResourceGroupName,
        [String] $AVSPrivateCloudName
    )
    $AzContext = Get-AzContext
    $AzureSubscriptionId = $AzContext.Subscription.Id
    Write-Debug "The default subscriptionId $AzureSubscriptionId of the current Azure context will be used."
    $AuthHeader = Get-AzureAuthHeader -AzContext $AzContext
    $AVSvCenterCredential = Get-AVSvCenterCredential -AuthHeader $AuthHeader -SubscriptionId $AzureSubscriptionId -AVSResourceGroupName $AVSResourceGroupName -AVSPrivateCloudName $AVSPrivateCloudName
    $AVSvCenterEndpoint = Get-AVSvCenterEndpoint  -AuthHeader $AuthHeader -SubscriptionId $AzureSubscriptionId -AVSResourceGroupName $AVSResourceGroupName -AVSPrivateCloudName $AVSPrivateCloudName
    $Credential = New-Object System.Management.Automation.PSCredential -ArgumentList ($AVSvCenterCredential.vCenterUsername, $(ConvertTo-SecureString $AVSvCenterCredential.vCenterPassword -AsPlainText -Force))
    # Try to connect to vcenter everytime when a cmdlet is invoked by the user to make sure right avs instance is connected
    $vCenterServer = Connect-VIserver -server $AVSvCenterEndpoint -Credential $Credential -ErrorAction Stop
    return $vCenterServer
}

function Connect-PureCloudBlockStore {
    Param (
        [Parameter(Mandatory=$false)]
        $PureCloudBlockStoreConnection
    )

    if (-not $PureCloudBlockStoreConnection) {
        $fa = $global:PURE_AUTH_2_X
        if (-not $fa) {
            throw "Please login to Cloud Clock Store using: Connect-Pfa2Array"
        }
    }
    else {
        $fa = $PureCloudBlockStoreConnection
    }

    $ClientName = "PureStorage.CBS.AVS"
    $module = Get-Module -Name $ClientName
    if ($module) {
      $Version = $module.Version.ToString()
    } else {
      $Version = "0.0.0.0"
    }

    # if was able to connect, also update the user agent to track backup sdk telemetry
    $success = $fa.UpdateUserAgent($ClientName, $Version );

    # this should not be a show stopper
    if(-not $success){
        Write-Warning -Level WARN -FunctionName $FunctionName -Msg "Failed to set useragent"
    }
    return $fa
}

function Purge-AzureSecretWithRetry {
    Param (
        [String] $KeyVaultName,
        [String] $SecretName
    )
    $errorOccurred = $true
    $retryCount = 0
    # Unfortunately we have to do retry because Remove-AzKeyVaultSecret is asyncronous. If purge happens directly after delete, "Secret is currently being deleted" error is thrown
    do {
        try {
            # To purge, use -InRemovedState parameter
            $retryCount = $retryCount + 1
            Remove-AzkeyVaultSecret -VaultName $KeyVaultName -Name $SecretName -InRemovedState -Force -ErrorAction Stop
            $errorOccurred = $false
        } catch {
            $errorOccurred = $true
            if ($retryCount -gt 3) {
                throw
            }
            Start-Sleep -Seconds 5
        }
    } while ($errorOccurred)
}

function Get-AvsClusterInformation {
    param (
        [String] $AvsResourceGroupName,
        [String] $AvsPrivateCloudName,
        [String] $AvsClusterName
    )
    $AzContext = Get-AzContext
    $AzureSubscriptionId = $AzContext.Subscription.Id
    Write-Debug "The default subscriptionId $AzureSubscriptionId of the current Azure context will be used."
    $RestUri = "https://management.azure.com/subscriptions/$($AzureSubscriptionId)/resourceGroups/$($avsResourceGroupName)/providers/Microsoft.AVS/privateClouds/$($avsPrivateCloudName)/clusters/$($avsClusterName)?api-version=2022-05-01"
    $Response = Invoke-AzRest -Uri $restUri -Method Get
    if (-not $Response -or $Response.StatusCode -ne 200) {
        throw "Failed to get AVS cluster information. Please make sure cluster $AvsClusterName exists. The cluster name would be case sensitive."
    }
    $result = $Response.Content | ConvertFrom-Json
    return $result
}

function Get-AvsClusterSku {
    param (
        [String] $AvsResourceGroupName,
        [String] $AvsPrivateCloudName,
        [String] $AvsClusterName
    )

    $result = Get-AvsClusterInformation -AvsResourceGroupName $AvsResourceGroupName -AvsPrivateCloudName $AvsPrivateCloudName -AvsClusterName $AvsClusterName
    return $result.sku.name
}

function Get-AVSClusterProvisionStatus {
    param (
        [String] $AvsResourceGroupName,
        [String] $AvsPrivateCloudName,
        [String] $AvsClusterName
    )

    $result = Get-AvsClusterInformation -AvsResourceGroupName $AvsResourceGroupName -AvsPrivateCloudName $AvsPrivateCloudName -AvsClusterName $AvsClusterName
    return $result.properties.provisioningState
}

$MPIO_SKUS = @(
    "av36", "av36t"
    "av36p", "av36pt"
    "av52", "av52t"
)
function Test-MPIOProvisionStatus {
    param (
        [Parameter(Mandatory=$true)]
        [String] $AvsResourceGroupName,
        [Parameter(Mandatory=$true)]
        [String] $AvsPrivateCloudName,
        [Parameter(Mandatory=$true)]
        [VMware.VimAutomation.ViCore.Types.V1.Inventory.Cluster]$Cluster

    )

    $sku = Get-AvsClusterSku -AvsResourceGroupName $AvsResourceGroupName -AvsPrivateCloudName $AvsPrivateCloudName -AvsClusterName $Cluster.Name
    if ($sku -in $MPIO_SKUS) {
        Write-Host "MPIO is supported for cluster $Cluster. Checking vmk interfaces...."
        $VMHosts = Get-VMHost -Location $Cluster
        foreach ($VMHost in $VMHosts) {
            $MpioAdapterOne = Get-VMHostNetworkAdapter -VMHost $VMHost | Where-Object {$_.Name -eq "vmk5"}
            $MpioAdapterTwo = Get-VMHostNetworkAdapter -VMHost $VMHost | Where-Object {$_.Name -eq "vmk6"}
            if ($MpioAdapterOne -and $MpioAdapterTwo) {
                Write-Host "MPIO is ready on $VMHost..."
            } else {
                throw "The current provisioning status of MPIO is not ready for host $VMHost. Please check if VMkernel adapter 'vmk5' and 'vmk6' are ready for host $VMhost. If not, please wait for twenty minutes and try again."
            }
        }
    } else {
        Write-Host "MPIO is not available for SKU $sku. Skipping MPIO check for cluster $Cluster..."
    }
}

function Get-RunCommandNamedOutput {
    param (
        [String] $RunCmdExecutionName,
        [String] $AvsResourceGroupName,
        [String] $AvsPrivateCloudName
    )
    $AzContext = Get-AzContext
    $AzureSubscriptionId = $AzContext.Subscription.Id
    Write-Debug "The default subscriptionId $AzureSubscriptionId of the current Azure context will be used."
    $AuthHeader = Get-AzureAuthHeader -AzContext $AzContext
    $RestUri = “https://management.azure.com/subscriptions/$($AzureSubscriptionId)/resourceGroups/$($avsResourceGroupName)/providers/Microsoft.AVS/privateClouds/$($avsPrivateCloudName)/scriptExecutions/$($RunCmdExecutionName)?api-version=2021-12-01”
    $Response = Invoke-RestMethod -Uri $restUri -Method Get -Headers $AuthHeader
    return $Response.properties.namedOutputs
}

function Get-Timeout {
    param (
        [Parameter(Mandatory=$true)]
        [string]$Cluster,

        [Parameter(Mandatory=$true)]
        $InputParams
    )

    if ($InputParams.ContainsKey("TimeoutInMinutes")) {
        return $InputParams["TimeoutInMinutes"]
    }
    $HostCount = (Get-VMHost -Location $Cluster).Count
    $Timeout = [Math]::Ceiling(($HostCount * $DEFAULT_PER_HOST_TIMEOUT_IN_SECONDS)/60)
    if ($Timeout -lt $MIN_TIMEOUT_IN_MINUTES ) {
        $Timeout = $MIN_TIMEOUT_IN_MINUTES
    }
    elseif ($Timeout -gt $MAX_TIMEOUT_IN_MINUTES){
        $Timeout = $MAX_TIMEOUT_IN_MINUTES
    }

    return $Timeout
}

function Test-RunCommandPackageAvailability {
    param (
        [Parameter(Mandatory = $true)]
        [String] $SubscriptionId,

        [Parameter(Mandatory = $true)]
        [String] $RunCommandModule,

        [Parameter(Mandatory = $true)]
        [String] $RunCommandPackageVersion,

        [Parameter(Mandatory = $false)]
        [String] $AVSCloudName,

        [Parameter(Mandatory = $false)]
        [String] $AVSResourceGroup
    )

    $Uri = "https://management.azure.com/subscriptions/$($SubscriptionId)/resourceGroups/$($AVSResourceGroup)/providers/Microsoft.AVS/privateClouds/$($AVSCloudName)/scriptPackages/$($RunCommandModule)@$($RunCommandPackageVersion)?api-version=2023-03-01"
    $Res = Invoke-AzRest -Uri $Uri -Method GET
    if ($Res.StatusCode -ne 200) {
        throw "Could not find '$($RunCommandModule)@$($RunCommandPackageVersion)' RunCommand package in Azure. Please make sure the package is available in Azure."
    }
    else {
        Write-Host "Found '$($RunCommandModule)@$($RunCommandPackageVersion)' RunCommand package in Azure..."
    }
}