RiverMeadow.Development.Source/SourceUtil/SourceUtil.psm1

using module '../../Common/Result'
Import-Module -Name @(Join-Path $PSScriptRoot .. | Join-Path -ChildPath .. | Join-Path -ChildPath Util | Join-Path -ChildPath Util)
Import-Module -Name @(Join-Path $PSScriptRoot .. | Join-Path -ChildPath .. | Join-Path -ChildPath Preflight | Join-Path -ChildPath Preflight)
function Get-RMSourcesForCurrentProject {
    param()

    $LoginStatus = Test-UserLoggedIn
    if ($LoginStatus.ReturnCode -eq [RMReturn]::ERROR) {
        return $LoginStatus
    }

    $CurrentProjectId = Get-Variable -Name "RMContext-CurrentProjectId" -ValueOnly

    $Result = @()
    $Response = Get-RMSourcesInternal -OrganizationId $CurrentProjectId -PageNumber 0
    $Result += $Response
    for ($index = 1; $index -lt $Response.page.totalPages; $index++) {
        $Result += Get-RMSourcesInternal -OrganizationId $CurrentProjectId -PageNumber $index
    }

    return $Result
}

function Get-RMVMSourceInternal {
    param (
        [string] $OrganizationId,
        [string] $VMFolderPath,
        [guid] $ApplianceId,
        [string] $VMName,
        [string] $CollectionType,
        [int] $PageNumber
    )
    

    $RMLoginResult = Get-Variable -Name "RMContext-UserLogin"
    $Uri = Get-Variable -Name "RMContext-ReactorURI" -ValueOnly

    $Headers = @{
        Accept = "application/rm+json"
        "X-Auth-Token" = $RMLoginResult.Value.token
    }

    if (![string]::IsNullOrWhiteSpace($VMFolderPath)) {
        $Uri = $Uri + "/organizations/" + $OrganizationId + "/sources?size=25&page=" + $PageNumber + "&sort=created_at%2Cdesc" + "&vm_folder_path=" + $VMFolderPath `
            + "&appliance_id=" + $ApplianceId + "&source_ip=" + $VMName + "&collection_type=" + $CollectionType
    } else {
        $Uri = $Uri + "/organizations/" + $OrganizationId + "/sources?size=25&page=" + $PageNumber + "&sort=created_at%2Cdesc" + "&appliance_id=" + $ApplianceId `
            + "&source_ip=" + $VMName + "&collection_type=" + $CollectionType
    }

    $Params = @{
        Method = "Get"
        Uri = $Uri
        Headers = $Headers
    }
    return Invoke-RMRestMethod -Params $Params
}

function Get-RMSourcesInternal {
    param(
        [string] $OrganizationId,
        [int] $PageNumber
    )

    $RMLoginResult = Get-Variable -Name "RMContext-UserLogin"
    $Uri = Get-Variable -Name "RMContext-ReactorURI"

    $Headers = @{
        Accept = "application/rm+json"
        "X-Auth-Token" = $RMLoginResult.Value.token
    }

    $Params = @{
        Method = "Get"
        Uri = $Uri.Value + "/organizations/" + $OrganizationId + "/sources?size=25&page=" + $PageNumber + "&sort=created_at%2Cdesc"
        Headers = $Headers
    }
    return Invoke-RMRestMethod -Params $Params
}

function Get-RMSourceById {
    param(
        [string] $SourceId
    )

    $RMLoginResult = Get-Variable -Name "RMContext-UserLogin"
    $Uri = Get-Variable -Name "RMContext-ReactorURI"

    $Headers = @{
        Accept = "application/rm+json"
        "X-Auth-Token" = $RMLoginResult.Value.token
    }

    $Params = @{
        Method = "Get"
        Uri = $Uri.Value + "/sources/" + $SourceId
        Headers = $Headers
    }
    return Invoke-RMRestMethod -Params $Params
}

function Get-MountPoint {
    param(
        [System.Object] $Source
    )
    $MountPoints = @()
    if ("windows" -ieq $Source.os_type) {
        foreach ($Mount in $Source.attributes.storage.mounts.psobject.properties.value) {
            $MountPoints += @{mount_point = $Mount.path}
        }
    } else {
        foreach ($Mount in $Source.attributes.storage.mounts.psobject.properties.value) {
            if ("disk" -ieq $Mount.nature -or "subvolume" -ieq $Mount.nature -and "squashfs" -ine $Mount.fs_type) {
                $MountPoints += @{mount_point = $Mount.path}
            }
        }
    }

    return $MountPoints
}

function Get-RMSourceByVMName {
    param (
        [string] $VMName,
        [string] $SourceVMFolderPath,
        [system.Object] $CloudAccount,
        [bool] $IsInteractive
    )
    
    $CurrentProjectId = Get-Variable -Name "RMContext-CurrentProjectId" -ValueOnly
    $PageIndex = 0
    while ($true) {
        $Response = Get-RMVMSourceInternal -OrganizationId $CurrentProjectId -PageNumber $PageIndex -VMFolderPath $SourceVMFolderPath `
            -ApplianceId $CloudAccount.appliance.id -VMName $VMName -CollectionType "VM"
        $Sources = $Response.Content

        if ($IsInteractive) {
            if ($Sources.Count -gt 1) {
                if ([string]::IsNullOrWhitespace($SourceVMFolderPath)) {
                    Write-RMError -Message "More than one VM was found with the VM name '$VMName', please provide the source VM folder path."
                    $SourceVMFolderPath = Read-RMString -UserMessage "Enter the source VM folder path" -ParameterName 'Source VM Folder Path' `
                        -IsRequired $false -DefaultValue 'None'
                    $PageIndex = 0
                    continue
                }
            }
        } else {
            if ($Sources.Count -gt 1) {
                if ([string]::IsNullOrWhitespace($SourceVMFolderPath)) {
                    Throw [System.Data.DuplicateNameException]::new(`
                        "More than one VM was found with the VM name '$VMName', please provide the parameter 'SourceVMFolderPath'.")
                }
            }
        }
        $PageIndex++
        if ($Sources.Count -eq 1) {
            return $Sources[0]
        } elseif ($Sources.Count -eq 0 || $PageIndex -ge $Response.page.totalPages) {
            break;
        }
    }

    $TargetCloud = $CloudAccount.name
    if ([string]::IsNullOrWhitespace($SourceVMFolderPath)) {
        throw [System.Management.Automation.ItemNotFoundException]::new(`
            "Could not find the source with the VM name '$VMName' that was discovered using the target cloud '$TargetCloud'.")
    } else {
        throw [System.Management.Automation.ItemNotFoundException]::new(`
            "Could not find the source with the VM name '$VMName' in the VM folder '$SourceVMFolderPath' that was discovered using the target cloud '$TargetCloud'."
        )
    }
}

function Get-RMSourceByIP {
    param(
        [string] $IPAddress
    )

    $CurrentProjectId = Get-Variable -Name "RMContext-CurrentProjectId" -ValueOnly

    $PageIndex = 0
    do {
        $Sources = Get-RMSourcesInternal -OrganizationId $CurrentProjectId -PageNumber $PageIndex
        foreach ($Source in $Sources.content) {
            if ($IPAddress -eq $Source.host) {
                return $Source
            }
        }
        $PageIndex++
    } while ($PageIndex -lt $Sources.page.totalPages)
    
    $ProjectName = Get-Variable -Name "RMContext-CurrentProjectName" -ValueOnly
    $OrgName = Get-Variable -Name "RMContext-CurrentOrganizationName" -ValueOnly
    throw [System.Management.Automation.ItemNotFoundException]::new(`
    "Source with IP address/VM name '$IPAddress' does not exist in project '$ProjectName' of organization '$OrgName', cannot start the migration. Please add the source in the project and try again.")
}

function Start-RMSourcePreflight {
    param (
        [System.Object] $Source,
        [System.Object] $CloudAccount,
        [string]  $AccountType
    )
    $RequestAttributes = @{
        "type" = "source"
        "resource_id"= $Source.id
        "overrides"= @{}
    }

    $RequestTargetCloudAttributes = @{
        "type" = "target_cloud"
        "resource_id" = $CloudAccount.id
        "migrate_netapp_files" = $false
        "cloud_type" = $AccountType
    }

    $SourceAttributesRequest = @($RequestAttributes, $RequestTargetCloudAttributes)
    $SourceAttributesRequestJson = ConvertTo-Json $SourceAttributesRequest

    $RMLoginResult = Get-Variable -Name "RMContext-UserLogin" -ValueOnly
    $Uri = Get-Variable -Name "RMContext-ReactorURI" -ValueOnly

    $Headers = @{
        Accept = "application/rm+json"
        "X-Auth-Token" = $RMLoginResult.token
    }

    $Params = @{
        Method = "Post"
        Uri = $Uri + "/preflights"
        Body = $SourceAttributesRequestJson
        ContentType = "application/json"
        Headers = $Headers
    }

    Write-Output "Starting source preflight..." | Out-Host
    return Invoke-RMRestMethod -Params $Params
}

function Get-RMSourceWithAttribute {
    param(
        [System.Object] $Source,
        [System.Object] $CloudAccount,
        [string] $AccountType,
        [bool] $IgnoreValidationErrors,
        [RMMigrationReturn] $RMMigrationReturn
    )
    if ($Source.attribute_state -eq "running") {
        Throw [System.InvalidOperationException]::New(
            "Source attribute collection state is 'running', please wait for the attribute collection to complete.")
    }

    Update-RMSourceWithAppliance -SourceId $Source.id -ApplianceId $CloudAccount.appliance.id
    $Response = Start-RMSourcePreflight -Source $Source -CloudAccount $CloudAccount -AccountType $AccountType
    Write-Output "Waiting for source preflight to complete..." | Out-Host
    $PreflightResult = Watch-RMPreflightStatus -PreflightId $Response.preflights[0].id `
        -TimeOutMessage "Timed out waiting for collection to complete"
    $Source = Get-RMSourceById -SourceId $Source.id
    if (![string]::IsNullOrEmpty($Source.attribute_error)) {
        $PreflightID = $Response.preflights[0].id
        $AttributeCollectionError = $Source.attribute_error
        Throw [System.Management.Automation.JobFailedException]::New(
            "Source attribute collection with ID: $PreflightID has failed with error $AttributeCollectionError.")
    }

    $ShouldExit, $PreflightWarning, $PreflightError = Out-RMPreflight -PreflightResult $PreflightResult -IgnoreValidationErrors $IgnoreValidationErrors -RMMigrationReturn $RMMigrationReturn
    $OverrideExistingMigrationWarning = $false
    $OverrideExistingMigrationError = $false
    if ($PreflightWarning.keys -contains "Conflicting Migration Attempts") {
        $OverrideExistingMigrationWarning = $true
    }
    
    if ($PreflightError.keys  -contains "Conflicting Migration Attempts") {
        $OverrideExistingMigrationError = $true
        if (1 -eq $PreflightError.Count) {
            $ShouldExit = $false
        }
    }
    return $Source, $ShouldExit, $OverrideExistingMigrationWarning, $OverrideExistingMigrationError
}

function Update-RMSourceWithAppliance {
    param (
        [System.Object] $SourceId,
        [string] $ApplianceId
    )
    
    $RequestAttributes = @{
        "data_only_migration" = $false
        "appliance_id" = $ApplianceId
    }

    $RequestAttributesJson = ConvertTo-Json $RequestAttributes

    $RMLoginResult = Get-Variable -Name "RMContext-UserLogin" -ValueOnly
    $Uri = Get-Variable -Name "RMContext-ReactorURI" -ValueOnly

    $Headers = @{
        Accept = "application/rm+json"
        "X-Auth-Token" = $RMLoginResult.token
    }

    $Params = @{
        Method = "Put"
        Uri = $Uri + "/sources/" + $SourceId
        Body = $RequestAttributesJson
        ContentType = "application/json"
        Headers = $Headers
    }

    Invoke-RMRestMethod -Params $Params | Out-Null
}

function Read-RMSource {
    param(
        [string] $UserMessage,
        [string] $ParameterName,
        [bool] $IsRequired
    )
    while ($true) {
        $SourceIP =  Read-RMIPAddress -UserMessage $UserMessage -ParameterName $ParameterName -IsRequired $IsRequired
        try {
            $Source = Get-RMSourceByIP -IPAddress $SourceIP
        } catch [System.Management.Automation.ItemNotFoundException] {
            $ProjectName = Get-Variable -Name "RMContext-CurrentProjectName" -ValueOnly
            $OrgName = Get-Variable -Name "RMContext-CurrentOrganizationName" -ValueOnly
            
            Write-RMError -Message "Source with IP address '$SourceIP' does not exist in project '$ProjectName' of organization '$OrgName'."
            continue
        }

        return $Source
    }
}

function Read-RMVMBasedSource {
    param(
        [string] $UserMessage,
        [string] $ParameterName,
        [System.Object] $CloudAccount,
        [bool] $IsRequired
    )
    while ($true) {
        $SourceVMName =  Read-RMString -UserMessage $UserMessage -ParameterName $ParameterName -IsRequired $IsRequired
        $SourceVMFolderPath = Read-RMString -UserMessage "Enter the source VM folder path" -ParameterName 'Source VM Folder Path' `
            -IsRequired $false -DefaultValue 'None'
        try {
            $Source = Get-RMSourceByVMName -VMName $SourceVMName -SourceVMFolderPath $SourceVMFolderPath  `
                    -CloudAccount $CloudAccount -IsInteractive $true
        } catch [System.Management.Automation.ItemNotFoundException] {
            Write-RMError -Message $PSItem.Exception.Message
            continue
        }
        return $Source
    }
}

function Get-RMVMBasedSelectedDisk {
    param(
        [System.Object] $Source,
        [string[]] $ExcludedDiskLabel
    )
    $MountPoints = @()
    for ($Index = 0; $Index -lt $Source.attributes.storage.vm_disks.Count; $Index++) {
        if ($ExcludedDiskLabel -contains $Source.attributes.storage.vm_disks[$Index].label) {
            continue
        }
        $MountPoints += @{mount_point = $Index.ToString()}
    }

    return $MountPoints
}

function Get-RMSelectedDiskByDiskLabel {
    param(
        [string[]] $DiskLabel,
        [System.Object] $Source
    )
    $MountPoints = @()
    for ($Index = 0; $Index -lt $Source.attributes.storage.vm_disks.Count; $Index++) {
        if ($DiskLabel -contains $Source.attributes.storage.vm_disks[$Index].label) {
            $MountPoints += @{mount_point = $Index.ToString()}
        }
    }

    return $MountPoints
}

function Get-RMVMDiskProperty {
    param (
        [System.Object] $Source
    )
    $Result = @()
    foreach ($VMDisk in $Source.attributes.storage.vm_disks) {
        $Size = [math]::round($VMDisk.size_kb/(1024 * 1024), 2)
        $Result += $VMDisk.label + " (Size: $Size GiB)"
    }

    return $Result
}

function Get-RMSelectedDisk {
    param(
        [System.Object] $Source
    )
    $DiskLabels = @()
    $Source.attributes.storage.vm_disks | ForEach-Object -Process { $DiskLabels += $_.label }
    while ($true) {
        $ReadTokens = Read-RMToken -UserMessage "Enter labels of the disks to be excluded, separated by commas" `
            -DefaultValue "None" -ParameterName "Disk label(s)" -Separator "," -IsRequired $false
        # Read-RMToken returns empty string when default None is used by the user, not ideal, should return empty array
        if ([string]::IsNullOrWhiteSpace($ReadTokens)) {
            return Get-RMVMBasedSelectedDisk -Source $Source -ExcludedDiskLabel $DisksToExclude
        }
        $DisksToExclude = @()
        foreach ($Token in $ReadTokens) {
            $Token = $Token.Trim('"')
            $Token = $Token.Trim("'")
            $DisksToExclude += $Token
        }
        $Result = Test-RMNonExistentMountPoints -SourceMountPoints $DiskLabels -UserInputMountPoints $DisksToExclude
        if ($Result.Count -gt 0) {
            $ResultAsString = $Result -join ", "
            Write-RMError -Message "Disk labels '$ResultAsString' does not exist on source and hence cannot be excluded, please try again."
            continue
        }

        if ($DisksToExclude.Count -eq $Source.attributes.storage.vm_disks.Count) {
            Write-RMError -Message "Cannot exclude all the disks, please try again."
            continue
        }

        return Get-RMVMBasedSelectedDisk -Source $Source -ExcludedDiskLabel $DisksToExclude
    }

}

function Test-RMSourceHasDynamicDisks {
    param(
        [System.Object] $Source
    )

    if ($Source.os_type -ine "windows") {
        return $false
    }

    foreach ($Disk in $Source.attributes.storage.disks.psobject.Properties.Value) {
        if ($null -ne $Disk.flags -and $Disk.flags -contains "dynamic_disk") {
            return $true
        }
    }
    return $false
}

function Confirm-RMSource {
    param (
       [string] $SourceId
    )

    $ErrorString = ""
    $Source = Get-RMSourceById -SourceId $SourceId
    $ProjectName = Get-Variable -Name "RMContext-CurrentProjectName" -ValueOnly
    $OrgName = Get-Variable -Name "RMContext-CurrentOrganizationName" -ValueOnly
    if ($null -eq $Source -or $Source.is_deleted) {
        if ($null -ne $Source) {
            $IPAddress = $Source.host
            $ErrorString = "Source with IP address/VM name '$IPAddress' does not exist in project '$ProjectName' of organization '$OrgName', cannot start the differential migration."
        } else {
            $ErrorString = "Source with ID '$SourceId' does not exist in project '$ProjectName' of organization '$OrgName', cannot start the differential migration."
        }
    } else {
        $ProjectId = Get-Variable -Name "RMContext-CurrentProjectId" -ValueOnly
        $IPAddress = $Source.host
        if ($Source.organization_id -ne $ProjectId) {
            $ErrorString = "Source with IP address/VM name '$IPAddress' does not exist in project '$ProjectName' of organization '$OrgName', if the source exists in a different project then switch to that project using the cmdlet 'Switch-RMProject' and then try again."
        }
    }
    return $Source, $ErrorString
    
}

# No Export-ModuleMember is being used which will automatically export all the functions
# of this module and we want all the functions to be exported.