CloudEndure.psm1

$script:URL = "https://console.cloudendure.com/api"
$script:Installer = "https://console.cloudendure.com/installer_win.exe"
$script:ProfileLocation = "$env:USERPROFILE\.cloudendure\credentials"

[System.Collections.Hashtable]$script:AWSRegionMap = @{
    "Generic" = "f54420d5-3de4-40bb-b35b-33d32ad8c8ef";
    "AWS US East (Northern Virginia)" = "71fbc661-a3a8-4315-bcf9-3db3353a6ef8";
    "AWS US West (Northern California)" = "959d856b-3730-48c2-84ba-a509497b2085";
    "AWS US West (Oregon)" = "31cc9e94-58af-4920-9cd7-db6a45f28fd4";
    "AWS EU (Ireland)" = "114b110d-00ad-48d4-a930-90cb3f8cde2e";
    "AWS EU (Frankfurt)" = "349db794-4bfc-4fc5-a733-659081d6729d";
    "AWS Asia Pacific (Singapore)" = "fc89da33-0eab-4602-8e4d-8c35b0ad8f65";
    "AWS Asia Pacific (Sydney)" = "4b4a4ff4-e5d9-4b62-8e7c-7210c9ea2be2";
    "AWS Asia Pacific (Tokyo)" = "acce3f71-3e7e-48db-bc5c-84d57f84f919";
    "AWS South America (Sao Paulo)" = "5dbb0a54-3361-4a6b-9dcb-1a1f87f4e1a2";
    "AWS US East (Ohio)" = "2941040c-a410-4bec-8842-566da8ca7729";
    "AWS Asia Pacific (Mumbai)" = "e5dfe786-736f-4f15-a737-7915e1a98349";
    "AWS Asia Pacific (Seoul)" = "1c805747-322d-4f04-b0cc-022a63baa824";
    "AWS EU (London)" = "0191fdf5-779f-4a54-a0ce-6e3b5157ce36";
    "AWS Canada (Central)" = "910a2cd6-0298-4c9b-82f5-74d6bd265211"
}

[System.Collections.Hashtable]$script:CloudIds = @{
    "AWS" = "6849e59c-29f5-4e10-a459-9d8584c7524b";
    "GENERIC" = "4e665c91-6dbc-4802-9832-85990d048852";
    "VCENTER" = "df35a75f-3b21-4161-aae0-4942d6e3b5f4";
    "On-Premises" = "00000000-0000-0000-0000-000000000000";
    "GCP" = "00000000-0000-0000-0000-000000000000";
    "Azure" = "00000000-0000-0000-0000-000000000000"
}

[System.Collections.Hashtable]$script:IdToCloudMap = @{
    "6849e59c-29f5-4e10-a459-9d8584c7524b" = "AWS";
    "4e665c91-6dbc-4802-9832-85990d048852" = "GENERIC";
    "df35a75f-3b21-4161-aae0-4942d6e3b5f4" = "VCENTER";
    "00000000-0000-0000-0000-000000000000" = "On-Premises";
    "00000000-0000-0000-0000-000000000001" = "GCP";
    "00000000-0000-0000-0000-000000000002" = "Azure"
}

# This is hashtable of hashtables
[System.Collections.Hashtable]$script:Sessions = @{}

[System.String[]]$script:CommonParams = [System.Management.Automation.PSCmdlet]::CommonParameters + [System.Management.Automation.PSCmdlet]::OptionalCommonParameters + @("PassThru", "Force", "Session", "ProjectId")
[System.String]$script:AllParameterSets = "__AllParameterSets"

#region Private Functions

Function Get-CESessionOrDefault {
    [CmdletBinding()]
    Param(
        [Parameter(Position = 0)]
        [System.String]$Session
    )

    Begin {

    }

    Process {
        $SessionInfo = $null

        if (-not [System.String]::IsNullOrEmpty($Session)) {
            $SessionInfo = $script:Sessions.Get_Item($Session)
        }
        else {
            $SessionInfo = $script:Sessions.GetEnumerator() | Select-Object -First 1 -ExpandProperty Value
            $Session = $SessionInfo.User.Username
        }

        if ($SessionInfo -eq $null) 
        {
            throw "A valid Session could not be found with the information provided. Check your active sessions with Get-CESession or create a new session with New-CESession."
        }

        Write-Output -InputObject $SessionInfo
    }

    End {

    }
}

Function Invoke-CERequest {
    [CmdletBinding()]
    Param(
        [Parameter(ParameterSetName = "Path")]
        [ValidatePattern("^/.*$")]
        [System.String]$Path,

        [Parameter(ParameterSetName = "Uri")]
        [ValidateNotNullOrEmpty()]
        [System.String]$Uri,

        [Parameter(Mandatory = $true)]
        [Microsoft.PowerShell.Commands.WebRequestMethod]$Method,

        [Parameter()]
        [System.String]$Session,

        [Parameter()]
        [ValidateRange(100, 599)]
        [System.Int32]$ExpectedResponseCode = 200,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]$Body = [System.String]::Empty,

        [Parameter()]
        [ScriptBlock]$ErrorHandling = $null,

        [Parameter()]
        [System.Object[]]$ErrorHandlingArgs = @()
    )

    Begin {
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        [System.Int32]$StatusCode = 0
        $Reason = ""
        $Splat = @{}

        if (-not [System.String]::IsNullOrEmpty($Body))
        {
            $Splat.Add("Body", $Body)
        }

        switch ($PSCmdlet.ParameterSetName)
        {
            "Path" {
                $Splat.Add("Path", $Path)
                break
            }
            "Uri" {
                $Splat.Add("Uri", $Uri)
                break
            }
        }

        # If a response generates a 300+ http response
        # it will set the error message
        # Hashtable has WebResponse, StatusCode, and ErrorMessage properties
        [System.Collections.Hashtable]$Result = Invoke-WebRequestWithBasicErrorHandling -Method $Method -Session $Session @Splat

        if ($ErrorHandling -eq $null)
        {
            if ($Result["StatusCode"] -eq $ExpectedResponseCode)
            {
                Write-Output -InputObject ([PSCustomObject](ConvertFrom-Json -InputObject $Result["WebResponse"].Content))
            }
            else
            {
                throw $Result["ErrorMessage"]
            }
        }
        else
        {
            & $ErrorHandling $Result["StatusCode"] $Result["WebResponse"].Content $Result["ErrorMessage"] $ErrorHandlingArgs
        }
    }

    End {
    }
}

Function Invoke-WebRequestWithBasicErrorHandling {
    [CmdletBinding(DefaultParameterSetName = "Path")]
    Param(
        [Parameter(ParameterSetName = "Path", Mandatory = $true)]
        [ValidatePattern("^/.*$")]
        [System.String]$Path,

        [Parameter(ParameterSetName = "Uri", Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Uri,

        [Parameter(Mandatory = $true)]
        [Microsoft.PowerShell.Commands.WebRequestMethod]$Method,

        [Parameter()]
        [System.String]$Session,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]$Body = [System.String]::Empty,

        [Parameter(ParameterSetName = "Uri")]
        [Switch]$Login
    )

    Begin {

        # No existing session info for a new session
        if (-not $Login)
        {
            $SessionInfo = Get-CESessionOrDefault -Session $Session
        }

        [System.Int32]$StatusCode = 0
        $Reason = ""
        $Splat = @{}

        if (-not [System.String]::IsNullOrEmpty($Body))
        {
            $Splat.Add("Body", $Body)
            $Splat.Add("ContentType", "application/json;charset=utf-8")
        }

        switch ($PSCmdlet.ParameterSetName)
        {
            "Path" {
                $Splat.Add("Uri", "$($SessionInfo.Url)$Path")
                break
            }
            "Uri" {
                $Splat.Add("Uri", $Uri)
                break
            }
        }

        $Headers = @{
            "Accept-Encoding" = "gzip, deflate, br";
            "Accept" = "application/json, text/plain, */*"
        }

        if ($Login)
        {
            $Splat.Add("SessionVariable", "WebSession")
        }

        try {
            [Microsoft.PowerShell.Commands.WebResponseObject]$Result = Invoke-WebRequest -Method $Method -WebSession $SessionInfo.WebSession -UseBasicParsing -ErrorAction Stop @Splat -Headers $Headers
            $StatusCode = $Result.StatusCode
            $Reason = $Result.StatusDescription
            Write-Verbose -Message $Result.Content
        }
        catch [System.Net.WebException] {
            [System.Net.WebException]$Ex = $_.Exception

            if ($Ex.Response -eq $null)
            {
                $Reason = "$($Ex.Status): $($Ex.Message)"
                $StatusCode = 500
            }
            else
            {
                [System.Net.HttpWebResponse]$Response = $Ex.Response
                $StatusCode = [System.Int32]$Response.StatusCode
            
                [System.IO.Stream]$Stream = $Response.GetResponseStream()
                [System.Text.Encoding]$Encoding = [System.Text.Encoding]::GetEncoding("utf-8")
                [System.IO.StreamReader]$Reader = New-Object -TypeName System.IO.StreamReader($Stream, $Encoding)
                $Content = $Reader.ReadToEnd()

                $Reason = "$($Response.StatusDescription) $($_.Exception.Message)`r`n$Content"
            }
        }
        catch [Exception]  {
            $Reason = $_.Exception.Message
        }

        $ErrorMessage = ""

        if ($StatusCode -ge 300)
        {
            $ErrorMessage = "$StatusCode : $Reason$(if (-not [System.String]::IsNullOrEmpty($Result.Content)) {"`n$($Result.Content)"})"
        }

        $ReturnData = @{"WebResponse" = $Result;
            "StatusCode" = $StatusCode;
            "ErrorMessage" = $ErrorMessage
        }

        if ($Login)
        {
            $ReturnData.Add("SessionVariable", $WebSession)
        }

        Write-Output -InputObject $ReturnData
    }
}

Function Convert-ParametersToHashtable {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [System.Collections.Generic.Dictionary[System.String, System.Management.Automation.ParameterMetadata]]$Parameters,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]$ParameterSetName = $script:AllParameterSets,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.RuntimeDefinedParameterDictionary]$RuntimeParameterDictionary,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [System.Collections.Generic.Dictionary[System.String, System.Object]]$BoundParameters,

        [Parameter()]
        [Switch]$IncludeAll,

        [Parameter()]
        [Switch]$IncludeDefaults,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]$FunctionName
    )

    Begin {
    }

    Process {
        $Params = @{}
        $Output = @{}

        # This is all of the parameters available for the function, get their names and parameter sets
        foreach ($Item in $Parameters.GetEnumerator() | Where-Object {-not $script:CommonParams.Contains($_.Key)})
        {
            [System.String[]]$Sets = $Item.Value.ParameterSets.GetEnumerator() | Select-Object -ExpandProperty Key
            $Params.Add($Item.Key, $Sets)
        }

        if ($RuntimeParameterDictionary -ne $null)
        {
            # Get the dynamic parameters, iterate on each
            $RuntimeParameterDictionary.GetEnumerator() | Where-Object {-not $script:CommonParams.Contains($_.Value.Name)} | ForEach-Object {

                # Get the parameter
                [System.Management.Automation.RuntimeDefinedParameter]$Param = $_.Value 

                # If the parameter hasn't already been added (don't need to check
                # if it was set since it wil be in the BoundParameters that we check
                # later)
                if (-not $Params.ContainsKey($Param.Name))
                {
                    # Get the parameter name and its parameter sets
                    [System.String[]]$ParameterSets = $Param.Attributes | Where-Object {$_ -is [System.Management.Automation.PARAMETERAttribute] } | Select-Object -ExpandProperty ParameterSetName
                    $Params.Add($Param.Name, $ParameterSets)
                }
            }
        }

        if ($IncludeDefaults -or $IncludeAll)
        {
            [System.Collections.Hashtable]$DefaultParameterValues = Get-DefaultParameterValues -FunctionName $FunctionName
        }

        # Now we have all of the parameters that are available in the function added to a dictionary
        # with their parameter sets

        # Iterate through these parameters and get their values if they are part of the Individual parameter set or is a parameter only part of __AllParameterSets
        foreach ($Item in ($Params.GetEnumerator() | 
            Where-Object {
                $_.Value.Contains($ParameterSetName) -or 
                ($_.Value.Length -eq 1 -and $_.Value.Contains($script:AllParameterSets)) -or
                $IncludeAll
            })
        )
        {
            $Add = $false
            $Value = $null

            # Check to see if it was supplied by the user, then it definitely
            if ($BoundParameters.ContainsKey($Item.Key))
            {
                $Add = $true
                $Value = $BoundParameters[$Item.Key]
            }
            # It was not specified by the user, but we might still want to add it
            else
            {
                # If not supplied, and we're including defaults, see if it has a default value
                if (($IncludeDefaults -or $IncludeAll) -and $DefaultParameterValues.ContainsKey($Item.Key))
                {
                    if ($Item.Key -eq "ReplicationTags")
                    {
                        Write-Host "Repl Tags Added"
                    }
                    $Add = $true
                    $Value = $DefaultParameterValues[$Item.Key]["Value"]
                }
                elseif ($IncludeAll)
                {
                    # Check this since it may have been added with a default value already
                    if ($Add -eq $false)
                    {
                        # This is the variable's type
                        $Name = $Parameters[$Item.Key].ParameterType.FullName
                        $Add = $true
                        Set-Variable $Item.Key -Scope 1 -Value (([System.Object]$BoundParameters[$Item.Key]) -as ($Name -as [Type]))
                    }
                }
            }

            if ($Add)
            {
                $Output.Add($Item.Key.Substring(0, 1).ToLower() + $Item.Key.Substring(1), $Value)
            }
        }

        Write-Output -InputObject $Output
    }

    End {
    }
}

Function Get-DefaultParameterValues {
    <#
 
    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [System.String]$FunctionName
    )

    Begin {
    }

    Process {
        $Ast = (Get-Command $FunctionName).ScriptBlock.Ast
        
        $Predicate = {
            Param(
                [System.Management.Automation.Language.Ast]$AstObject
            )

            return ($AstObject -is [System.Management.Automation.Language.ParamBlockAst])
        }
        
        [System.Management.Automation.Language.ParamBlockAst[]]$ParamBlocks = $Ast.FindAll($Predicate, $true)

        $Params = @{}

        $ParamBlocks.Parameters | ForEach-Object {
            if ($_.DefaultValue)
            {
                $Type = ($_.StaticType.FullName -as [Type])
                $Value = (Invoke-Expression -Command $_.DefaultValue.Extent.Text) -as $Type
                $Params.Add($_.Name.VariablePath.UserPath, @{"Value" = $Value; "Type" = $Type })
            }
        }

        Write-Output -InputObject $Params
    }

    End {
    }
}

#endregion

#region Profiles

Function New-CEProfile {
    <#
        .SYNOPSIS
            Saves a new CloudEndure profile.
 
        .DESCRIPTION
            The cmdlet saves a username and password (encrypted using the Windows DPAPI under the current user's context) to a file at a specified profile location.
            The profile credentials can be called later during the New-CESession cmdlet to simplify remembering credentials.
 
        .PARAMETER Credential
            The credentials to save.
 
        .PARAMTER ProfileName
            The name of the profile.
 
        .PARAMETER ProfileLocation
            Specify a non-default location to store the profile file. This defaults to $env:USERPROFILE\.cloudendure\credentials
 
        .EXAMPLE
            New-CEProfile -ProfileName "MyCEProfile" -Credential (New-Object -TypeName System.Management.Automation.PSCredential("john.smith@contoso.com", (ConvertTo-SecureString -String "My$ecurEP@$$w0Rd" -AsPlainText -Force))
 
            This saves a new profile named MyCEProfile with the specified credentials.
 
        .INPUTS
            System.Management.Automation.PSCredential
                 
        .OUTPUTS
            None
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/11/2019
    #>

    # Default to ApiToken so when someone specifies the ApiToken parameter
    # it won't be confused on whether or not credential is required as well
    [CmdletBinding(DefaultParameterSetName = "ApiToken")]
    [OutputType()]
    Param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "Creds")]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [PSCredential]::Empty,

        [Parameter(Mandatory = $true, ParameterSetName = "ApiToken")]
        [Parameter(ParameterSetName = "Creds")]
        [ValidateNotNullOrEmpty()]
        [System.String]$ApiToken = [System.String]::Empty,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]$ProfileName,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]$ProfileLocation = [System.String]::Empty
    )

    Begin {
    }

    Process {
        if ([System.String]::IsNullOrEmpty($ProfileLocation)) 
        {
            $ProfileLocation = $script:ProfileLocation
        }

        Write-Verbose -Message "Using profile location $ProfileLocation."

        if (-not (Test-Path -Path $ProfileLocation -PathType Leaf))
        {
            Write-Verbose -Message "Creating new credentials file at $ProfileLocation."
            New-Item -Path $ProfileLocation -ItemType File -Force
        }
        
        switch ($PSCmdlet.ParameterSetName)
        {
            "Creds" {
                $CEProfile = @{"username" = $Credential.UserName; 
                    "password" = ConvertFrom-SecureString -SecureString $Credential.Password; 
                    "apitoken" = $ApiToken.Replace("-", "")}

                break
            }
            "ApiToken" {
                $CEProfile = @{"username" = ""; 
                    "password" = ""; 
                    "apitoken" = $ApiToken.Replace("-", "")}
                break
            }
            default {
                throw "$($PSCmdlet.ParameterSetName) is not a recognized parameter set."
            }
        }

        # This will store the password encrypted with the Windows DPAPI using the user's credentials
        
        $Content = Get-Content -Path $ProfileLocation -Raw

        if ($Content -ne $null -and -not [System.String]::IsNullOrEmpty($Content))
        {
            [PSCustomObject]$Json = ConvertFrom-Json -InputObject $Content
        }
        else
        {
            [PSCustomObject]$Json = [PSCustomObject]@{}
        }

        if ((Get-Member -InputObject $Json -Name $ProfileName -MemberType Properties) -ne $null) 
        {
            Write-Warning -Message "The profile $ProfileName is being overwritten with new data."
            $Json.$ProfileName =  $CEProfile
        }
        else 
        {
            $Json | Add-Member -MemberType NoteProperty -Name $ProfileName -Value $CEProfile
        }

        Set-Content -Path $ProfileLocation -Value (ConvertTo-Json -InputObject $Json)
        Write-Verbose -Message "Successfully saved credentials."
    }

    End {
    }
}

Function Get-CEProfile {
    <#
        .SYNOPSIS
            Gets profile information.
 
        .DESCRIPTION
            This cmdlet retrieves a list of available profile names if no profile name is specified. If a profilename is specified, a PSCredential object is returned back if the profile is found.
 
        .PARAMETER ProfileName
            The name of the profile to retrieve. If this is not specified, a list of available profile names is returned.
 
        .PARAMETER ProfileLocation
            The location of the profile credential data. This defaults to $env:USERPROFILE\.cloudendure\credentials.
 
        .EXAMPLE
            Get-CEProfile
             
            This returns a list of available profiles.
 
        .EXAMPLE
            Get-CEProfile -ProfileName MyCEProfile
 
            This returns a PSCredential object with the credentials stored as MyCEProfile using the New-CEProfile cmdlet.
 
        .INPUTS
            System.String
 
        .OUTPUTS
            System.String[] or PSCustomObject
 
            If no profile is specified, an array of profile names is returned. If the profile name is specified, the a PSCustomObject with a PSCredential and API Token string are retured.
 
            {
                "credential" : PSCredential,
                "apitoken" : "tokenstring"
            }
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/11/2019
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject], [System.String[]])]
    Param(
        [Parameter(Position = 0, ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]$ProfileName = [System.String]::Empty,

        [Parameter(Position = 1)]
        [ValidateNotNullOrEmpty()]
        [System.String]$ProfileLocation = [System.String]::Empty
    )

    Begin {
    }

    Process {
        if ([System.String]::IsNullOrEmpty($ProfileLocation)) 
        {
            $ProfileLocation = $script:ProfileLocation
        }

        if (-not (Test-Path -Path $ProfileLocation -PathType Leaf))
        {
            Write-Warning -Message "No profile data stored at $ProfileLocation"
            Exit
        }

        $Content = Get-Content -Path $ProfileLocation -Raw

        if ($Content -ne $null -and -not [System.String]::IsNullOrEmpty($Content))
        {
            [PSCustomObject]$Json = ConvertFrom-Json -InputObject $Content
        }
        else
        {
            [PSCustomObject]$Json = [PSCustomObject]@{}
        }

        if (-not [System.String]::IsNullOrEmpty($ProfileName))
        {
            # This is the profile data including the credential and/or api token
            if (-not ($Json | Get-Member -MemberType Properties -Name $ProfileName))
            {
                throw "A profile with the name $ProfileName could not be found in $ProfileLocation."
            }

            $Value = $Json.$ProfileName

            if (
                # If either username or password are missing
                (
                    (-not ($Value | Get-Member -MemberType Properties -Name username)) -or
                    (-not ($Value | Get-Member -MemberType Properties -Name password))
                ) -and
                # And api token is missing or empty, throw an exception
                (
                    (-not ($Value | Get-Member -MemberType Properties -Name apitoken)) -or
                    ($Value.apitoken -eq [System.String]::Empty)
                )
            )
            {
                throw "Profile $ProfileName does not have properly configured credentials.`n$($Value | ConvertTo-Json)"
            }
                
            # Convert the stored data back to a PSCredential object
            $Creds = New-Object -TypeName System.Management.Automation.PSCredential($Json.$ProfileName.username, (ConvertTo-SecureString -String $Json.$ProfileName.password))
            [PSCustomObject]$CEProfile = [PSCustomObject]@{"credential" = $Creds; "apitoken" = ""}

            if (-not [System.String]::IsNullOrEmpty($Json.$ProfileName.apitoken))
            {
                $CEProfile.apitoken = Convert-SecureStringToString -SecureString (ConvertTo-SecureString -String $Json.$ProfileName.apitoken)
            }

            Write-Output -InputObject $CEProfile
        }
        else 
        {
            # This will return all of the "keys" which are the profile names
            Write-Output -InputObject ($Json | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name)
        }    
    }

    End {
    }
}

Function Update-CEProfile {
    <#
        .SYNOPSIS
            Updates an existing CE Profile.
 
        .DESCRIPTION
            Updates an existing CE Profile with a new username and password and/or API Token.
 
        .PARAMETER Credential
            The credentials to save.
 
        .PARAMETER ApiToken
            The API Token to save.
 
        .PARAMTER ProfileName
            The name of the profile.
 
        .PARAMETER ProfileLocation
            Specify a non-default location to store the profile file. This defaults to $env:USERPROFILE\.cloudendure\credentials
 
        .EXAMPLE
            Update-CEProfile -ProfileName MyCEProfile -Credential -Credential (New-Object -TypeName System.Management.Automation.PSCredential("john.smith@contoso.com", (ConvertTo-SecureString -String "MyNEW$ecurEP@$$w0Rd" -AsPlainText -Force))
 
        .INPUTS
            None
                 
        .OUTPUTS
            None
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/11/2019
    #>

    [CmdletBinding()]
    [OutputType()]
    Param(
        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [PSCredential]::Empty,

        [Parameter()]
        [ValidateNotNull()]
        [System.String]$ApiToken,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]$ProfileName,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]$ProfileLocation = [System.String]::Empty
    )

    Begin {
    }

    Process {
        if ([System.String]::IsNullOrEmpty($ProfileLocation)) 
        {
            $ProfileLocation = $script:ProfileLocation
        }

        Write-Verbose -Message "Using profile location $ProfileLocation."

        if (-not (Test-Path -Path $ProfileLocation -PathType Leaf))
        {
            Write-Warning -Message "Profile data could not be found at $ProfileLocation."
            Exit
        }

        $Content = Get-Content -Path $ProfileLocation -Raw

        if ($Content -ne $null -and -not [System.String]::IsNullOrEmpty($Content))
        {
            [PSCustomObject]$Json = ConvertFrom-Json -InputObject $Content
        }
        else
        {
            throw "No profiles found in $ProfileLocation."
        }

        # This is the profile data including the credential and/or api token
        if (-not ($Json | Get-Member -MemberType Properties -Name $ProfileName))
        {
            throw "A profile with the name $ProfileName could not be found in $ProfileLocation."
        }

        $Value = $Json.$ProfileName

        if ($Credential -ne [System.Management.Automation.PSCredential]::Empty)
        {
            if (-not ($Value | Get-Member -MemberType NoteProperty -Name username))
            {
                $Value | Add-Member -Name username -MemberType NoteProperty -Value ""
            }

            if (-not ($Value | Get-Member -MemberType NoteProperty -Name password))
            {
                $Value | Add-Member -Name password -MemberType NoteProperty -Value ""
            }

            $Value.username = $Credential.UserName
            $Value.password = ConvertFrom-SecureString -SecureString $Credential.Password
        }

        if (-not [System.String]::IsNullOrEmpty($ApiToken))
        {
            if (-not ($Value | Get-Member -MemberType NoteProperty -Name apitoken))
            {
                $Value | Add-Member -Name apitoken -MemberType NoteProperty -Value ""
            }

            $Value.apitoken = ConvertTo-SecureString -String $ApiToken.Replace("-", "") -AsPlainText -Force | ConvertFrom-SecureString
        }

        $Json.$ProfileName = $Value

        Set-Content -Path $ProfileLocation -Value (ConvertTo-Json -InputObject $Json)
        Write-Verbose -Message "Successfully saved credentials."
    }

    End {
    }
}

Function Remove-CEProfile {
    <#
        .SYNOPSIS
            Removes a CE profile.
 
        .DESCRIPTION
            This cmdlet retrieves the specified profile and deletes it from the credentials file.
 
        .PARAMETER ProfileName
            The name of the profile to remove.
 
        .PARAMETER ProfileLocation
            The location of the profile credential data. This defaults to $env:USERPROFILE\.cloudendure\credentials.
 
        .PARAMETER PassThru
            If specified, the deleted profile is returned as a PSCredential object.
 
        .EXAMPLE
            Remove-CEProfile -ProfileName "MyCEProfile"
 
            Removes the MyCEProfile profile.
 
        .INPUTS
            System.String
 
        .OUTPUTS
            None or System.Management.Automation.PSCredential
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/15/2019
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "HIGH")]
    [OutputType([System.Management.Automation.PSCredential])]
    Param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]$ProfileName = [System.String]::Empty,

        [Parameter(Position = 1)]
        [ValidateNotNullOrEmpty()]
        [System.String]$ProfileLocation = [System.String]::Empty,

        [Parameter()]
        [Switch]$PassThru,

        [Parameter()]
        [Switch]$Force
    )

    Begin {
    }

    Process {
        if ([System.String]::IsNullOrEmpty($ProfileLocation)) 
        {
            $ProfileLocation = $script:ProfileLocation
        }

        if (Test-Path -Path $ProfileLocation -PathType Leaf)
        {
            $Content = Get-Content -Path $ProfileLocation -Raw

            if ($Content -ne $null -and -not [System.String]::IsNullOrEmpty($Content))
            {
                [PSCustomObject]$Json = ConvertFrom-Json -InputObject $Content
                $Value = $Json | Get-Member -MemberType Properties -Name $ProfileName
                
                if ($Value -ne $null)
                {
                    # Convert the stored data back to a PSCredential object
                    $Creds = New-Object -TypeName System.Management.Automation.PSCredential($Json.$ProfileName.username, (ConvertTo-SecureString -String $Json.$ProfileName.password))

                    $ConfirmMessage = "You are about to delete profile $ProfileName."
                    $WhatIfDescription = "Deleted profile $ProfileName"
                    $ConfirmCaption = "Delete CE Profile"

                    if ($Force -or $PSCmdlet.ShouldProcess($WhatIfDescription, $ConfirmMessage, $ConfirmCaption))
                    {
                        # This returns void, so do it first, then pass the $Json variable
                        $Json.PSObject.Properties.Remove($ProfileName)
                        Set-Content -Path $ProfileLocation -Value (ConvertTo-Json -InputObject $Json)

                        if ($PassThru) 
                        {
                            Write-Output -InputObject $Creds
                        }
                    }
                }
                else
                {
                    Write-Warning -Message "No profile matching $ProfileName in $ProfileLocation"
                }
            }
        }
        else
        {
            Write-Warning -Message "No profile data stored at $ProfileLocation"
        }
    }

    End {
    }
}

#endregion

#region Sessions

Function New-CESession {
    <#
        .SYNOPSIS
            Establishes a new session with the CE console
 
        .DESCRIPTION
            The cmdlet establishes a new session with the CE console and saves the session information to local script variables. These can be cleared with the Remove-CESession cmdlet.
 
        .PARAMETER Version
            The version of the API this session will use. This defaults to "LATEST".
 
            This parameter is deprecated, the CE API set handles selecting the correct version for you based on your account and this parameter has no effect.
 
        .PARAMETER Credential
            The credential to use to connect to the CE console.
 
        .PARAMETER ApiToken
            The user API Token to use to login.
 
        .PARAMETER GoogleOAuthCode
            The Google OAuth code used to login.
 
        .PARAMETER ProfileName
            The name of the profile to use.
 
        .PARAMETER IgnoreSslErrors
            Used if you want to ignore SSL errors, for example if testing locally with a proxy. USE CAUTION with this parameter.
 
        .PARAMETER PassThru
            If specified, the session unique identifier, the CE username, will be returned. This can be specified directly to follow-on cmdlets to specify which account the cmdlet targets.
 
        .EXAMPLE
            New-CESession -Credential (New-Object -TypeName System.Management.Automation.PSCredential("myfirstmigration@cloudendure.com", (ConvertTo-SecureString -String "mySecureP@$$w0rd" -AsPlainText -Force)))
 
            Establishes a new session to CE with the supplied email address and password. The session information is stored in script variables.
 
        .INPUTS
            None
 
        .OUTPUTS
            None or System.String
 
            If PassThru is specified the username being used to store the session data is returned.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/15/2019
    #>

    [CmdletBinding(DefaultParameterSetName = "Profile")]
    [OutputType([System.String])]
    Param(
        [Parameter(Mandatory = $true, ParameterSetName = "Credential")]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty,

        [Parameter(Mandatory = $true, ParameterSetName = "ApiToken")]
        [ValidateNotNullOrEmpty()]
        [System.String]$ApiToken,

        [Parameter(Mandatory = $true, ParameterSetName = "GoogleOAuth")]
        [ValidateNotNullOrEmpty()]
        [System.String]$GoogleOAuthCode,

        [Parameter()]
        [Switch]$IgnoreSslErrors,
 
        [Parameter()]
        [Switch]$PassThru
    )

    DynamicParam {
        if (
            ($Credential -eq $null -or $Credential -eq [System.Management.Automation.PSCredential]::Empty) -and 
            [System.String]::IsNullOrEmpty($ApiToken) -and
            [System.String]::IsNullOrEmpty($GoogleOAuthCode)
        )
        {
            $Params = @(
                @{
                    "Name" = "ProfileName";
                    "Type" = [System.String];
                    "ValidateSet" = (Get-CEProfile);
                    "ParameterSets" = @("Profile");
                    "Mandatory" = $true;
                    "ValidateNotNullOrEmpty" = $true
                },
                @{
                    "Name" = "ProfileLocation";
                    "Type" = [System.String];
                    "ParameterSets" = @("Profile");
                    "ValidateNotNullOrEmpty" = $true
                }
            )

            $Params | ForEach-Object {
                New-Object PSObject -Property $_ 
            } | New-DynamicParameter
        }
    }

    Begin {
    }

    Process {
        $Body = @{}

        switch ($PSCmdlet.ParameterSetName)
        {
            "ApiToken" {
                $Body.Add("userApiToken", $ApiToken)
                break
            }
            "Profile" {
                $Splat = @{"ProfileName" = $PSBoundParameters["ProfileName"]}

                if (-not [System.String]::IsNullOrEmpty($PSBoundParameters["ProfileLocation"]))
                {
                    $Splat.Add("ProfileLocation", $PSBoundParameters["ProfileLocation"])
                }

                # This will safety check to make sure the profile has
                # credentials, it will throw an exception if the profile does not
                $Creds = Get-CEProfile @Splat

                if ($Creds -eq $null)
                {
                    throw "Could not find a profile named $($PSBoundParameters["ProfileName"])."
                }

                if (($Creds | Get-Member -MemberType Properties -Name apitoken) -and (-not [System.String]::IsNullOrEmpty($Creds.apitoken)))
                {
                    $Body.Add("userApiToken", $Creds.apitoken)
                }
                else
                {
                    $Credential = $Creds.credential
                    $Body.Add("username", $Credential.UserName) 
                    $Body.Add("password", (Convert-SecureStringToString -SecureString $Credential.Password))
                }

                break
            }
            "Credential" {
                $Body.Add("username", $Credential.UserName) 
                $Body.Add("password", (Convert-SecureStringToString -SecureString $Credential.Password))
                break
            }
            "GoogleOAuth" {
                $Body.Add("googleOauthCode", $GoogleOAuthCode)
                break
            }
            default {
                throw "Unrecognized parameter set name $($PSCmdlet.ParameterSetName)."
            }
        }

        # Always logon to latest
        [System.String]$Uri = "$script:URL/latest/login"

        <#
            {
                "username": "user@example.com",
                "loginToken": "string",
                "userApiToken": "string",
                "agentInstallationToken": "string",
                "password": "pa$$word",
                "accountIdentifier": "string",
                "googleOauthCode": "string"
            }
        #>


        if ($IgnoreSslErrors)
        {
            [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
        }

        [System.Collections.Hashtable]$Data = Invoke-WebRequestWithBasicErrorHandling -Uri $Uri -Method Post -Body (ConvertTo-Json -InputObject $Body) -Login
        [Microsoft.PowerShell.Commands.WebResponseObject]$Result = $Data["WebResponse"]

        switch ($Result.StatusCode)
        {
            200 {
                $Version = $Result.Headers["X-CloudEndure-Version"]
                
                # Login can return a redirect to a specific API version endpoint, grab this redirected endpoint from the response and save
                # it to use on all subsequent requests
                [System.String]$Url = $Result.BaseResponse.ResponseUri.ToString().Substring(0,  $Result.BaseResponse.ResponseUri.ToString().LastIndexOf("/"))

                if ($Url.EndsWith("latest"))
                {
                    $Url = $Url.Substring(0, $Url.LastIndexOf("/")) + "/$Version"
                }

                [Microsoft.PowerShell.Commands.WebRequestSession]$WebSession = $Data["SessionVariable"]

                <#
                    {
                        "username": "user@example.com",
                        "status": "PENDING",
                        "account": "string",
                        "roles": [
                            "USER"
                        ],
                        "settings": {
                            "sendNotifications": {
                                "projectIDs": [
                                            "string"
                                ]
                            }
                        },
                        "apiToken": "string",
                        "hasPassword": true,
                        "termsAccepted": true,
                        "id": "string",
                        "selfLink": "string"
                    }
                #>

                $LoginResponse = ConvertFrom-Json -InputObject $Result.Content

                # Update Websession variable to persist credentials and headers
                if ($Credential -ne [System.Management.Automation.PSCredential]::Empty)
                {
                    $WebSession.Credentials = $Credential
                }

                # Add functionality for new XSRF token included in login for v3, which now must be presented
                # in the header of each request
                [System.Net.CookieCollection]$Cookies = $WebSession.Cookies.GetCookies($Url)
                [System.Net.Cookie]$MatchingCookie = $Cookies | Where-Object {$_.Name -ieq "XSRF-TOKEN"} | Select-Object -First 1

                if ($MatchingCookie -ne $null)
                {
                    $WebSession.Headers.Add("X-XSRF-TOKEN", $MatchingCookie.Value)
                }

                #[System.Collections.Hashtable]$Session = @{Session = $WebSession; ProjectId = $Summary.Projects.Items[0].Id; DefaultProject = $Summary.Projects.Items[0]; DefaultCloudCredentials = $Summary.Projects.Items[0].CloudCredentialsIDs[0]; User = $Summary.User; }
                [System.Collections.Hashtable]$Session = @{
                    WebSession = $WebSession; 
                    Url = $Url; 
                    DefaultProjectId = "";
                    UserId = "";
                    DefaultCloudCredentialsId = "";
                    User = $LoginResponse; 
                    Version = $Version;
                    GenericRegionId = "";
                }    

                if ($script:Sessions.ContainsKey($LoginResponse.Username)) {
                    $script:Sessions.Set_Item($LoginResponse.Username.ToLower(), $Session)
                }
                else {
                    $script:Sessions.Add($LoginResponse.Username.ToLower(), $Session)
                }

                # Set Project Id Defaults
                $ExtendedInfo = Get-CEAccountExtendedInfo -Session $LoginResponse.Username.ToLower()

                $Session["DefaultProjectId"] = $ExtendedInfo.projects.items[0].id
                $Session["UserId"] = $ExtendedInfo.user.id
                $Session["DefaultCloudCredentialsId"] = $ExtendedInfo.projects.items[0].cloudCredentialsIDs[0]
                $Session["GenericRegionId"] = $ExtendedInfo.genericRegion.id
                $script:Sessions.Set_Item($LoginResponse.Username.ToLower(), $Session)

                if ($PassThru) {
                    Write-Output -InputObject $LoginResponse.Username.ToLower()
                }
    
                break
            }
            401 {
                throw "The login credentials provided cannot be authenticated"
            }
            402 {
                throw "There is no active license configured for this account (A license must be purchased or extended)." 
            }
            429 {
                throw "Authentication failure limit has been reached. The service will become available for additional requests after a timeout."
            }
            default {
                throw "The login failed for an unknown reason: $($Data["ErrorMessage"])"
            }
        }
    }

    End {
    }
}

Function Get-CESession {
    <#
        .SYNOPSIS
            Gets stored CE session information.
 
        .DESCRIPTION
            The cmdlet retrieves an established CE session by its Id, or lists all active sessions. If a session name is supplied and cannot be found this function returns null.
 
        .PARAMETER Session
            Specifies the unique identifier of the session to query. If this parameter is not specified, all stored sessions are returned.
 
        .EXAMPLE
            Get-CESession
 
            Gets all CE session information stored in the script variable.
 
        .INPUTS
            None or System.String
 
        .OUTPUTS
            System.Collections.Hashtable
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/15/2019
    #>

    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    Param(
        [Parameter(ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }

    Process {
        if (-not [System.String]::IsNullOrEmpty($Session)) {
            if ($script:Sessions.ContainsKey($Session)) {
                Write-Output -InputObject $script:Sessions.Get_Item($Session)
            }
            else {
                Write-Output -InputObject $null
            }
        }
        else {
            Write-Output -InputObject $script:Sessions
        }
    }

    End {
    }
}

Function Remove-CESession {
    <#
        .SYNOPSIS
            Removes stored CE session information
 
        .DESCRIPTION
            The cmdlet removes CE session information generated by the New-CESession cmdlet.
 
        .PARAMETER Session
            Specifies the unique identifier of the session to remove. If this parameter is not specified, all stored sessions are removed.
 
        .EXAMPLE
            Remove-CESession
 
            Removes all CE session information stored in the script variable.
 
        .INPUTS
            None or System.String
 
        .OUTPUTS
            None
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/15/2019
    #>

    [CmdletBinding()]
    [OutputType()]
    Param(
        [Parameter(ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }

    Process {
        $SessionsToEnd = @()

        if ($Session -eq [System.String]::Empty)
        {
            foreach ($SessionInfo in $script:Sessions.GetEnumerator())
            {
                $SessionsToEnd += $SessionInfo.Key
            }
        }
        else
        {
            $SessionsToEnd += $Session
        }

        foreach ($SessionToEnd in $SessionsToEnd)
        {
            $SessionInfo = $Sessions[$SessionToEnd]

            Write-Verbose -Message "Terminating session for $SessionToEnd."

            $Uri = "$($SessionInfo.Url)/logout"

            $StatusCode = 0
            $Reason = ""

            try {
                [Microsoft.PowerShell.Commands.WebResponseObject]$Result = Invoke-WebRequest -Uri $Uri -Method Post -WebSession $SessionInfo.WebSession -UseBasicParsing -ErrorAction Stop
                $StatusCode = $Result.StatusCode
                $Reason = $Result.StatusDescription
            }
            catch [System.Net.WebException] {
                [System.Net.WebException]$Ex = $_.Exception

                if ($Ex.Response -eq $null)
                {
                    $Reason = "$($Ex.Status): $($Ex.Message)"
                    $StatusCode = 500
                }
                else
                {
                    [System.Net.HttpWebResponse]$Response = $Ex.Response
                    $StatusCode = [System.Int32]$Response.StatusCode
            
                    [System.IO.Stream]$Stream = $Response.GetResponseStream()
                    [System.Text.Encoding]$Encoding = [System.Text.Encoding]::GetEncoding("utf-8")
                    [System.IO.StreamReader]$Reader = New-Object -TypeName System.IO.StreamReader($Stream, $Encoding)
                    $Content = $Reader.ReadToEnd()

                    $Reason = "$($Response.StatusDescription) $($_.Exception.Message)`r`n$Content"
                }
            }
            catch [Exception]  {
                $Reason = $_.Exception.Message
            }

            $script:Sessions.Remove($SessionToEnd.ToLower())

            if ($StatusCode -ne 204)
            {
                Write-Warning -Message "Problem terminating session for $SessionToEnd`: $StatusCode $Reason - $($Result.Content)"
            }
            else
            {
                Write-Verbose -Message "Successfully removed session $SessionToEnd."
            }
        }
    }

    End {
    }
}

#endregion
    `
#region Blueprints

Function New-CEBlueprint {
    <#
        .SYNOPSIS
            Define the target machine characteristics: machine and disk types, network configuration, etc.
 
            This cmdlet is only used when migrating from AWS to AWS. It is not used for DR or migrating from outside AWS into AWS.
 
        .DESCRIPTION
            This cmdlet defines the target machine characteristics: machine and disk types, network configuration, etc. There can be only one blueprint per machine per region. Returns the newly created object.
 
        .PARAMETER Blueprint
            The blueprint to apply, the hashtable can be defined with the following data (this is presented in JSON, which the hashtable will be converted to):
 
            If you specify a blueprint document, all other configuration parameters are ignored.
 
            {
              "iamRole": "string",
              "scsiAdapterType": "string",
              "publicIPAction": "ALLOCATE",
              "machineName": "string",
              "cpus": 0,
              "securityGroupIDs": [
                "string"
              ],
              "runAfterLaunch": true,
              "networkInterface": "string",
              "mbRam": 0,
              "instanceType": "string",
              "subnetIDs": [
                "string"
              ],
              "coresPerCpu": 0,
              "staticIp": "string",
              "tags": [
                {
                  "key": "string",
                  "value": "string"
                }
              ],
              "securityGroupAction": "FROM_POLICY",
              "privateIPs": [
                "string"
              ],
              "tenancy": "SHARED",
              "computeLocationId": "string",
              "subnetsHostProject": "string",
              "logicalLocationId": "string",
              "networkAdapterType": "string",
              "byolOnDedicatedInstance": true,
              "placementGroup": "string",
              "disks": [
                {
                  "iops": 0,
                  "type": "COPY_ORIGIN",
                  "name": "string"
                }
              ],
              "privateIPAction": "CREATE_NEW",
              "staticIpAction": "EXISTING",
              "dedicatedHostIdentifier": "string",
              "useSharedRam": true
            }
     
        .PARAMETER Path
            The path to a file containing the JSON definition of the blueprint.
 
        .PARAMETER IAMRole
            AWS only. The AWS IAM Role to associate with this blueprint.
 
        .PARAMETER ScsiAdapterType
            VCENTER Only. The scsi adapter type.
 
        .PARAMETER PublicIPAction
            Whether to allocate an ephemeral public IP, or not. AS_SUBNET causes CloudEndure to copy this property from the source machine.
     
        .PARAMETER MachineName
            The instance to create this blueprint for.
 
        .PARAMETER Cpus
            VCENTER Only. Number of CPUs per target machine.
 
        .PARAMETER SecurityGroupIds
            AWS Only. The security groups that will be associated with the instance.
 
        .PARAMETER RunAfterLaunch
            AWS Only. Specify true to have the instance started after it is launched or false to leave it in a stopped state.
 
        .PARAMETER NetworkInterface
            VCENTER Only. The network interface to use.
 
        .PARAMETER MBRAM
            VCENTER Only. The network interface to use.
 
        .PARAMETER InstanceType
            The instance type to launch the replica as.
 
        .PARAMETER SubnetIDs
            Specify the subnet Id(s) the instance will be associated with.
 
        .PARAMETER CoresPerCpu
            VCENTER Only. The number of cores per CPU.
 
        .PARAMETER StaticIP
            If you select ALLOCATE for StaticIPAction, then specify Elatic IP address to associate with the instance.
 
        .PARAMETER Tags
            AWS only. Tags that will be applied to the target machine. This parameter must specify Key and Value. For example:
 
            @(@{Key = "name"; Value = "my server"}, @{Key = "env"; Value = "dev"})
 
        .PARAMETER SecurityGroupAction
            Currently only supports the value "FROM_POLICY".
 
        .PARAMETER PrivateIPs
            If you select CUSTOM for PrivateIPAction, specify the private IPs you want associated with the instance.
 
        .PARAMETER Tenancy
            The tenancy of the replica.
 
        .PARAMETER ComputeLocationId
            VCENTER only.
 
        .PARAMETER SubnetsHostProject
            GCP only. Host project for cross project network subnet.
 
        .PARAMETER LogicalLocationId
            VCENTER only. vcenter = vmFolder; relates to $ref LogicalLocation
 
        .PARAMETER NetworkAdapterType
            VCENTER only. The type of network adapter to use.
 
        .PARAMETER BYOLOnDedicatedInstance
            AWS only. Specifies whether to use byol windows license if dedicated instance tenancy is selected.
 
        .PARAMETER PlacementGroup
            AWS Only. The placement group to launch the instance in.
 
        .PARAMETER Disks
            AWS only. Target machine disk properties. An array of objects with properties as follows:
 
                IOPS: Int >= 0
                TYPE: "COPY_ORIGIN", "STANDARD", "SSD", "PROVISIONED_SSD", "ST1", "SC1"
                NAME: Disk name as appears in the source machine object.
 
        .PARAMETER PrivateIPAction
            The action for the instance's private IP address.
 
        .PARAMETER StaticIpAction
            The action for the instance's static IP address.
 
        .PARAMETER DedicatedHostIdentifier
            AWS only. The Id for the dedicated host.
 
        .PARAMETER UseSharedRAM
            VCENTER only. Specifies whether to use shared RAM for the replica.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .PARAMETER PassThru
            Passes through the created object.
 
        .PARAMETER ProjectId
            The project Id to use to retrieve the details. This defaults to the current project retrieved from the login.
 
        .EXAMPLE
            New-CEBlueprint -MachineName "MyTestMachine" -IAMRole EC2StandardInstanceProfile -SubnetIDs @("subnet-152acf5d") -SecurityGroupIDs ("sg-6053bf1f")
 
            Creates a new blueprint and associates an AWS IAM Role, a specific deployment subnet, and a security group belonging to the VPC containing the subnet.
 
        .EXAMPLE
            New-CEBlueprint -MachineName "MyTestMachine" -SubnetIDs @("Default")
             
            Deploys the machine into the default subnet for configured target region.
 
        .INPUTS
            None or System.Collections.Hashtable
 
        .OUTPUTS
           None or System.Management.Automation.PSCustomObject
 
            The JSON representation of the return value:
 
            {
              "iamRole": "string",
              "scsiAdapterType": "string",
              "publicIPAction": "ALLOCATE",
              "machineName": "string",
              "cpus": 0,
              "securityGroupIDs": [
                "string"
              ],
              "runAfterLaunch": true,
              "recommendedPrivateIP": "string",
              "networkInterface": "string",
              "id": "string",
              "mbRam": 0,
              "instanceType": "string",
              "subnetIDs": [
                "string"
              ],
              "coresPerCpu": 0,
              "recommendedInstanceType": "string",
              "staticIp": "string",
              "tags": [
                {
                  "key": "string",
                  "value": "string"
                }
              ],
              "securityGroupAction": "FROM_POLICY",
              "privateIPs": [
                "string"
              ],
              "tenancy": "SHARED",
              "computeLocationId": "string",
              "subnetsHostProject": "string",
              "logicalLocationId": "string",
              "networkAdapterType": "string",
              "byolOnDedicatedInstance": true,
              "placementGroup": "string",
              "machineId": "string",
              "region": "string",
              "disks": [
                {
                  "iops": 0,
                  "type": "COPY_ORIGIN",
                  "name": "string"
                }
              ],
              "privateIPAction": "CREATE_NEW",
              "staticIpAction": "EXISTING",
              "dedicatedHostIdentifier": "string",
              "useSharedRam": true
            }
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/16/2019
             
    #>

    [CmdletBinding(DefaultParameterSetName="__AllParameterSets")]
    [OutputType([System.Management.Automation.PSCustomObject])]
    Param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0, ParameterSetName = "Blueprint")]
        [ValidateNotNull()]
        [System.Collections.Hashtable]$Blueprint = @{},

        [Parameter(Mandatory = $true, ParameterSetName = "BlueprintFile")]
        [ValidateScript({
            Test-Path $_
        })]
        [System.String]$Path,

        [Parameter()]
        [ValidateSet("ALLOCATE", "DONT_ALLOCATE", "AS_SUBENT")]
        [System.String]$PublicIPAction,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]$MachineName,

        [Parameter(ParameterSetName = "AWS")]
        [System.Boolean]$RunAfterLaunch = $true,

        [Parameter(ParameterSetName = "VCENTER")]
        [ValidateNotNullOrEmpty()]
        [System.String]$NetworkInterface,

        [Parameter(ParameterSetName = "VCENTER")]
        [ValidateRange(0, 32)]
        [System.Int32]$CoresPerCpu,        

        [Parameter(ParameterSetName = "AWS")]
        [ValidateNotNull()]
        [System.Collections.Hashtable[]]$Tags = @(),

        [Parameter()]
        [ValidateSet("FROM_POLICY")]
        [System.String]$SecurityGroupAction = "FROM_POLICY",

        [Parameter()]
        [ValidateSet("SHARED", "DEDICATED", "HOST")]
        [System.String]$Tenancy,

        [Parameter(ParameterSetName = "VCENTER")]
        [ValidateNotNullOrEmpty()]
        [System.String]$ComputeLocationId,

        [Parameter(ParameterSetName = "GCP")]
        [ValidateNotNullOrEmpty()]
        [System.String]$SubnetsHostProject,

        [Parameter(ParameterSetName = "VCENTER")]
        [ValidateNotNullOrEmpty()]
        [System.String]$LogicalLocationId = "CLOUDENDURE",

        [Parameter(ParameterSetName = "AWS")]
        [System.Boolean]$BYOLOnDedicatedInstance = $false,

        [Parameter(ParameterSetName = "AWS")]
        [ValidateNotNull()]
        [System.Collections.Hashtable[]]$Disks,

        [Parameter()]
        [ValidateSet("CREATE_NEW", "COPY_ORIGIN", "CUSTOM_IP")]
        [System.String]$PrivateIPAction,

        [Parameter()]
        [ValidateSet("EXISTING", "DONT_CREATE", "CREATE_NEW", "IF_IN_ORIGIN")]
        [System.String]$StaticIPAction,

        [Parameter(ParameterSetName = "AWS")]
        [ValidateNotNullOrEmpty()]
        [System.String]$DedicatedHostIdentifier = "AUTO_PLACEMENT",

        [Parameter(ParameterSetName = "VCENTER")]
        [System.Boolean]$UseSharedRam,

        [Parameter()]
        [Switch]$PassThru,

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    DynamicParam {

        # Create the dictionary
        [System.Management.Automation.RuntimeDefinedParameterDictionary]$RuntimeParameterDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary

        # Only generate the dynamic parameters if a blueprint doc wasn't specified
        if (-not $PSBoundParameters.ContainsKey("Blueprint") -and -not $PSBoundParameters.ContainsKey("Path"))
        {
            $DynSplat = @{}

            if (-not [System.String]::IsNullOrEmpty($Session)) {
                $DynSplat.Add("Session", $Session)
            }

            if ($ProjectId -ne $null -and $ProjectId -ne [System.Guid]::Empty)
            {
                $DynSplat.Add("ProjectId", $ProjectId)
            }

            [PSCustomObject]$TargetCloudRegion = Get-CETargetCloudRegion @DynSplat

            $InstanceTypes = $TargetCloudRegion.InstanceTypes
            $InstanceTypes += "COPY_ORIGIN"
            $InstanceTypes += "CUSTOM"

            New-DynamicParameter -Name "InstanceType" -Type ([System.String]) -ValidateSet $InstanceTypes -Mandatory -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null

            if ($PrivateIPAction -ieq "CUSTOM_IP")
            {
                New-DynamicParameter -Name "PrivateIPs" -Type ([System.String[]]) -ValidateNotNullOrEmpty -Mandatory -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
            }

            if ($StaticIPAction -ieq "EXISTING")
            {
                New-DynamicParameter -Name "StaticIP" -Type ([System.String]) -Mandatory -ValidateSet $TargetCloudRegion.StaticIPs -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
            }

            if ($TargetCloudRegion -ne "GENERIC")
            {
                $CloudName = Get-CECloud -Id $TargetCloudRegion.Cloud | Select-Object -ExpandProperty Name
            }
            else
            {
                $CloudName = $TargetCloudRegion
            }
            
            switch ($CloudName)
            {
                "AWS" {

                    if ($TargetCloudRegion.IAMRoles.Length -gt 0)
                    {
                        New-DynamicParameter -Name "IAMRole" -Type ([System.String]) -ParameterSets @("AWS") -ValidateSet $TargetCloudRegion.IAMRoles -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
                    }

                    if ($TargetCloudRegion.PlacementGroups.Length -gt 0)
                    {
                        New-DynamicParameter -Name "PlacementGroup" -Type ([System.String]) -ValidateSet ($TargetCloudRegion.PlacementGroups) -ParameterSets @("AWS") -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
                    }

                    if ($TargetCloudRegion.Subnets.Length -gt 0)
                    {
                        $SubnetSet = $TargetCloudRegion.Subnets | Where-Object {$_.SubnetId -ne $null } | Select-Object -ExpandProperty SubnetId
                        # Add default to allow user to specify the default subnet for the configured region
                        $SubnetSet += "Default"

                        New-DynamicParameter -Name "SubnetIDs" -Type ([System.String[]]) -ValidateSet $SubnetSet -ParameterSets @("AWS") -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
                    }

                    $Type = Import-UnboundParameterCode -PassThru
                    $Subnets = $Type.GetMethod("GetUnboundParameterValue").MakeGenericMethod([System.Object]).Invoke($Type, @($PSCmdlet, "SubnetIDs", -1))

                    # Get the first subnet Id
                    $Key = [System.String]::Empty

                    if ($Subnets -is [System.Array])
                    {
                        $Key = $Subnets[0]
                    }
                    elseif ($Subnets -is [System.String])
                    {
                        if (-not [System.String]::IsNullOrEmpty($Subnets))
                        {
                            $Key = $Subnets
                        }
                    }

                    # Attempt to get the first subnet from the target region object
                    $Subnet = $TargetCloudRegion.Subnets | Where-Object {$_.Name -ieq $Key -or $_.SubnetId -ieq $Key} | Select-Object -First 1 -ErrorAction SilentlyContinue

                    # If the subnet is "Default", you won't be able to select a security group, so a new one will be created
                    # Make sure there are security groups in this region and that we found a matching one
                    # Subnet will be null if user selected "Default"
                    if ($TargetCloudRegion.SecurityGroups -ne $null -and $TargetCloudRegion.SecurityGroups.Length -gt 0 -and $Subnet -ne $null)
                    {
                        # Get the network Id based on the selected subnet so we can get the right security groups as options
                        [System.String[]]$SGSet = $TargetCloudRegion.SecurityGroups | Where-Object {$_.NetworkId -ieq $Subnet.NetworkId} | Select-Object -ExpandProperty SecurityGroupId

                        New-DynamicParameter -Name "SecurityGroupIDs" -Type ([System.String[]]) -ParameterSets @("AWS") -ValidateSet $SGSet -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
                    }

                    break
                }
                "GCP" {

                    break
                }
                "Azure" {

                    break
                }
                "VCENTER" {
                    if ($TargetCloudRegion.scsiAdapterTypes.Length -gt 0)
                    {
                        New-DynamicParameter -Name "SCSIAdapterType" -Type ([System.String]) -ParameterSets @("VCENTER") -ValidateSet $TargetCloudRegion.scsiAdapterTypes -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
                    }

                    New-DynamicParameter -Name "Cpus" -Type ([System.Int32]) -ParameterSets @("VCENTER") -ValidateRange @(1, $TargetCloudRegion.maxCoresPerMachineCpu) -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null

                    if ($TargetCloudRegion.networkAdapterTypes.Length -gt 0)
                    {
                        New-DynamicParameter -Name "NetworkAdapterType" -Type ([System.String]) -ParameterSets @("VCENTER") -ValidateSet $TargetCloudRegion.networkAdapterTypes -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
                    }

                    New-DynamicParameter -Name "CoresPerCpu" -Type ([System.Int32]) -ParameterSets @("VCENTER") -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
                    
                    New-DynamicParameter -Name "MBRAM" -Type ([System.Int32]) -ParameterSets @("VCENTER") -ValidateSet $TargetCloudRegion.maxCoresPerMachineCpu -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
                    
                    break
                }
                "GENERIC" {

                    break
                }
                default {
                    throw "The cloud environment $CloudName is not supported by this cmdlet yet."
                    break
                }
            }
        }

        return $RuntimeParameterDictionary
    }

    Begin {
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        $Body = ""
        $BlueprintObject = [PSCustomObject]@{}
        switch ($PSCmdlet.ParameterSetName)
        {
            "BlueprintFile" {
                $BlueprintObject = Get-Content -Path $Path -Raw | ConvertFrom-Json
                break
            }
            "Blueprint" {
                $BlueprintObject = [PSCustomObject]$Blueprint
                break
            }
            default {
                # This is going to take all of the parameters supplied, put them into a hash table, and then create a json
                # document that is the blueprint
                $Blueprint = Convert-ParametersToHashtable -Parameters (Get-Command -Name $PSCmdlet.MyInvocation.InvocationName).Parameters `
                    -ParameterSetName $PSCmdlet.ParameterSetName `
                    -RuntimeParameterDictionary $RuntimeParameterDictionary `
                    -BoundParameters $PSBoundParameters 

                if ($BluePrint["StaticIPAction"] -ne "EXISTING") {
                    $BluePrint["staticIP"] = ""
                }

                if ($BluePrint["PrivateIPAction"] -ne "CUSTOM_IP") {
                    $BluePrint["privateIPs"] = @()
                }

                $BlueprintObject = [PSCustomObject]$Blueprint
            }
        }

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.DefaultProjectId
        }

        [System.String]$UrlPath = "/projects/$ProjectId/blueprints"
        $Body = ConvertTo-Json -InputObject $BlueprintObject

        Write-Verbose -Message "Creating new blueprint:`r`n$Body"

        try {
            $Result = Invoke-CERequest -Path $UrlPath -Body $Body -Method Post -Session $Session -ExpectedResponseCode 201
            Write-Verbose -Message "Blueprint successfully created."

            if ($PassThru)
            {
                Write-Output -InputObject $Result
            }
        }
        catch [Exception] {
            throw "There was an issue creating the new blueprint: $($_.Exception.Message)"
        }    
    }

    End {
    }
}

Function Get-CEBlueprint {
    <#
        .SYNOPSIS
            Gets blueprint information.
 
        .DESCRIPTION
            The cmdlet retrieves a specific blueprint or a lost of blueprints of the specified or default project if no Id is provided.
 
        .PARAMETER Id
            The blueprint Id to retrieve. If this parameter is not specified, the blueprints are listed.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .PARAMETER Offset
            With which item to start (0 based).
 
        .PARAMETER Limit
            A number specifying how many entries to return between 0 and 1500 (defaults to 1500).
 
        .PARAMETER ProjectId
            The project Id to use to retrieve the details. This defaults to the current project retrieved from the login.
 
        .PARAMETER All
            Gets all blueprints without paging the results.
             
        .EXAMPLE
            Get-CEBlueprint
 
            Retrieves the blueprints (up to 1500 starting at index 0) of the default project.
 
        .EXAMPLE
            Get-CEBlueprint -Offset 1501 -Limit 10
 
            Retrieves the blueprints at index 1501 through 1511. This skips listing the first 1501 blueprints (offset is a 0 based index).
 
        .EXAMPLE
            Get-CEBlueprint -Id 184142f8-a581-4c86-9285-e24382d60d55
 
            Gets the blueprint matching the provided Id.
 
        .INPUTS
            None or System.Guid
 
        .OUTPUTS
           System.Management.Automation.PSCustomObject[] or System.Management.Automation.PSCustomObject
 
            The JSON representation of the array:
 
            [
                {
                  "iamRole": "string",
                  "scsiAdapterType": "string",
                  "publicIPAction": "ALLOCATE",
                  "machineName": "string",
                  "cpus": 0,
                  "securityGroupIDs": [
                    "string"
                  ],
                  "runAfterLaunch": true,
                  "recommendedPrivateIP": "string",
                  "networkInterface": "string",
                  "id": "string",
                  "mbRam": 0,
                  "instanceType": "string",
                  "subnetIDs": [
                    "string"
                  ],
                  "coresPerCpu": 0,
                  "recommendedInstanceType": "string",
                  "staticIp": "string",
                  "tags": [
                    {
                      "key": "string",
                      "value": "string"
                    }
                  ],
                  "securityGroupAction": "FROM_POLICY",
                  "privateIPs": [
                    "string"
                  ],
                  "tenancy": "SHARED",
                  "computeLocationId": "string",
                  "subnetsHostProject": "string",
                  "logicalLocationId": "string",
                  "networkAdapterType": "string",
                  "byolOnDedicatedInstance": true,
                  "placementGroup": "string",
                  "machineId": "string",
                  "region": "string",
                  "disks": [
                    {
                      "iops": 0,
                      "type": "COPY_ORIGIN",
                      "name": "string"
                    }
                  ],
                  "privateIPAction": "CREATE_NEW",
                  "staticIpAction": "EXISTING",
                  "dedicatedHostIdentifier": "string",
                  "useSharedRam": true
                }
            ]
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/17/2019
    #>

    [CmdletBinding(DefaultParameterSetName = "List")]
    [OutputType([PSCustomObject], [PSCustomObject[]])]
    Param(
        [Parameter(ValueFromPipeline = $true, Mandatory = $true, Position = 0, ParameterSetName = "Get")]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$Id = [System.Guid]::Empty,

        [Parameter(ParameterSetName = "List")]
        [ValidateRange(0, [System.UInt32]::MaxValue)]
        [System.UInt32]$Offset = 0,

        [Parameter(ParameterSetName = "List")]
        [ValidateRange(0, 1500)]
        [System.UInt32]$Limit = 1500,

        [Parameter(ParameterSetName = "All")]
        [Switch]$All,

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {        
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session
        
        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.DefaultProjectId
        }

        [System.String]$Path = "/projects/$ProjectId/blueprints"

        [PSCustomObject[]]$Results = @()

        switch ($PSCmdlet.ParameterSetName)
        {
            "Get" {
                if ($Id -ne [System.Guid]::Empty)
                {
                    $Path += "/$($Id.ToString())"
                }

                $ErrorHandling = {
                    Param($StatusCode, $Content, $ErrorMessage, $BlueprintId)
                
                    switch ($StatusCode)
                    {
                        200 {
                            Write-Output -InputObject $Content
                            break
                        }
                        404 {
                            throw "Could not find a blueprint with id $BlueprintId."
                        }
                        default {
                            throw $ErrorMessage
                        }            
                    }
                }

                try {
                    $Result = Invoke-CERequest -Path $Path -Method Get -Session $Session -ErrorHandling $ErrorHandling -ErrorHandlingArgs @($Id)
                }
                catch [Exception] {
                    throw "There was an issue retrieving blueprints: $($_.Exception.Message)."
                }

                $Results += $Result

                break
            }
            "List" {

                # If non default values for either were specified, update the URI
                if ($Offset -gt 0 -or $Limit -lt 1500)
                {
                    $QueryString = [System.String]::Empty

                    if ($Offset -gt 0)
                    {
                        $QueryString += "&offset=$Offset"
                    }

                    if ($Limit -lt 1500)
                    {
                        $QueryString += "&limit=$Limit"
                    }

                    # Remove the first character which is an unecessary ampersand
                    $Path += "?$($QueryString.Substring(1))"
                }

                try {
                    $Result = Invoke-CERequest -Method Get -Path $Path -Session $Session -ExpectedResponseCode 200
                    $Results += ($Result | Select-Object -ExpandProperty items)
                }
                catch [Exception] {
                    throw "There was an issue retrieving blueprints: $($_.Exception.Message)"
                }
                
                break
            }
            "All" {

                $Offset = 0
                $Limit = 1500
                [System.Int32]$ResultCount = 0

                # Go until the results returned are less than the specified limit
                do
                {
                    Write-Verbose -Message "Querying blueprints from $Offset to $($Offset + $Limit)."

                    [System.String]$QueryString = "?offset=$Offset&limit=$Limit"
                    [System.String]$TempUri = "$Path$QueryString"
                    
                    try {
                        $Result = Invoke-CERequest -Method Get -Path $TempUri -Session $Session -ExpectedResponseCode 200
                        $Results += ($Result | Select-Object -ExpandProperty items)
                        $ResultCount = $Result.Length
                        $Offset += $Limit
                    }
                    catch [Exception] {
                        throw "There was an issue retrieving blueprints: $($_.Exception.Message)"
                    }
                } while ($ResultCount -ge $Limit)

                break
            }
            default {
                throw "Encountered an unknown parameter set $($PSCmdlet.ParameterSetName)."
                break
            }
        }

        if ($PSCmdlet.ParameterSetName -eq "Get")
        {
            Write-Output -InputObject $Results[0]
        }
        else
        {            
            ,$Results
        }
    }

    End {
    }
}

Function Set-CEBlueprint {
    <#
        .SYNOPSIS
            Sets a blueprint for a CE Instance.
 
        .DESCRIPTION
            The cmdlet updates the blueprint for a specific CE Instance. Set a parameter to an empty string to clear it from the blueprint.
 
            Currently, this cmdlet only supports AWS target cloud environments.
 
        .PARAMETER Blueprint
            The updated blueprint data to send. This hashtable only needs to contain the data that you want to update. The original blueprint will be merged with this one.
 
            If you specify a blueprint, all other configuration parameters are ignored.
 
            The available configuration items are:
 
            {
              "iamRole": "string",
              "scsiAdapterType": "string",
              "publicIPAction": "ALLOCATE",
              "machineName": "string",
              "cpus": 0,
              "securityGroupIDs": [
                "string"
              ],
              "runAfterLaunch": true,
              "networkInterface": "string",
              "mbRam": 0,
              "instanceType": "string",
              "subnetIDs": [
                "string"
              ],
              "coresPerCpu": 0,
              "staticIp": "string",
              "tags": [
                {
                  "key": "string",
                  "value": "string"
                }
              ],
              "securityGroupAction": "FROM_POLICY",
              "privateIPs": [
                "string"
              ],
              "tenancy": "SHARED",
              "computeLocationId": "string",
              "subnetsHostProject": "string",
              "logicalLocationId": "string",
              "networkAdapterType": "string",
              "byolOnDedicatedInstance": true,
              "placementGroup": "string",
              "disks": [
                {
                  "iops": 0,
                  "type": "COPY_ORIGIN",
                  "name": "string"
                }
              ],
              "privateIPAction": "CREATE_NEW",
              "staticIpAction": "EXISTING",
              "dedicatedHostIdentifier": "string",
              "useSharedRam": true
            }
 
        .PARAMETER Path
            The path to a file containing the JSON definition of the blueprint.
 
        .PARAMETER IAMRole
            AWS only. The AWS IAM Role to associate with this blueprint.
 
        .PARAMETER ScsiAdapterType
            VCENTER Only. The scsi adapter type.
 
        .PARAMETER PublicIPAction
            Whether to allocate an ephemeral public IP, or not. AS_SUBNET causes CloudEndure to copy this property from the source machine.
     
        .PARAMETER MachineName
            The instance to create this blueprint for.
 
        .PARAMETER Cpus
            VCENTER Only. Number of CPUs per target machine.
 
        .PARAMETER SecurityGroupIds
            AWS Only. The security groups that will be associated with the instance.
 
        .PARAMETER RunAfterLaunch
            AWS Only. Specify true to have the instance started after it is launched or false to leave it in a stopped state.
 
        .PARAMETER NetworkInterface
            VCENTER Only. The network interface to use.
 
        .PARAMETER MBRAM
            VCENTER Only. The network interface to use.
 
        .PARAMETER InstanceType
            The instance type to launch the replica as.
 
        .PARAMETER SubnetIDs
            Specify the subnet Id(s) the instance will be associated with.
 
        .PARAMETER CoresPerCpu
            VCENTER Only. The number of cores per CPU.
 
        .PARAMETER StaticIP
            If you select ALLOCATE for StaticIPAction, then specify Elatic IP address to associate with the instance.
 
        .PARAMETER Tags
            AWS only. Tags that will be applied to the target machine. This parameter must specify Key and Value. For example:
 
            @(@{Key = "name"; Value = "my server"}, @{Key = "env"; Value = "dev"})
 
        .PARAMETER SecurityGroupAction
            Currently only supports the value "FROM_POLICY".
 
        .PARAMETER PrivateIPs
            If you select CUSTOM for PrivateIPAction, specify the private IPs you want associated with the instance.
 
        .PARAMETER Tenancy
            The tenancy of the replica.
 
        .PARAMETER ComputeLocationId
            VCENTER only.
 
        .PARAMETER SubnetsHostProject
            GCP only. Host project for cross project network subnet.
 
        .PARAMETER LogicalLocationId
            VCENTER only. vcenter = vmFolder; relates to $ref LogicalLocation
 
        .PARAMETER NetworkAdapterType
            VCENTER only. The type of network adapter to use.
 
        .PARAMETER BYOLOnDedicatedInstance
            AWS only. Specifies whether to use byol windows license if dedicated instance tenancy is selected.
 
        .PARAMETER PlacementGroup
            AWS Only. The placement group to launch the instance in.
 
        .PARAMETER Disks
            AWS only. Target machine disk properties. An array of objects with properties as follows:
 
                IOPS: Int >= 0
                TYPE: "COPY_ORIGIN", "STANDARD", "SSD", "PROVISIONED_SSD", "ST1", "SC1"
                NAME: Disk name as appears in the source machine object.
 
        .PARAMETER PrivateIPAction
            The action for the instance's private IP address.
 
        .PARAMETER StaticIpAction
            The action for the instance's static IP address.
 
        .PARAMETER DedicatedHostIdentifier
            AWS only. The Id for the dedicated host.
 
        .PARAMETER UseSharedRAM
            VCENTER only. Specifies whether to use shared RAM for the replica.
 
        .PARAMETER InstanceId
            The id of the CE instance whose blueprint you want to update.
 
        .PARAMETER BlueprintId
            The id of the CE blueprint you want to update.
 
        .PARAMETER ProjectId
            The project Id to use to retrieve the details. This defaults to the current project retrieved from the login.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .PARAMETER PassThru
            The updated blueprint will be returned to the pipeline.
 
        .EXAMPLE
            Set-CEBlueprint -InstanceId 47d842b8-ebfa-4695-90f8-fb9ab686c708 -Blueprint @{"IAMRole" = "EC2-InstanceProfile-Public"}
 
            This adds or updates the IAMRole property for the blueprint to "EC2-InstanceProfile-Public" for the CE instance identified by 47d842b8-ebfa-4695-90f8-fb9ab686c708.
 
        .EXAMPLE
            Set-CEBlueprint -InstanceId 47d842b8-ebfa-4695-90f8-fb9ab686c708 -IAMRole "EC2-InstanceProfile-Public"
 
            This adds or updates the IAMRole property for the blueprint to "EC2-InstanceProfile-Public" for the CE instance identified by 47d842b8-ebfa-4695-90f8-fb9ab686c708.
 
        .INPUTS
            None or System.Collections.Hashtable
 
        .OUTPUTS
           None or System.Management.Automation.PSCustomObject
 
            The JSON representation of the return value:
            {
              "iamRole": "string",
              "scsiAdapterType": "string",
              "publicIPAction": "ALLOCATE",
              "machineName": "string",
              "cpus": 0,
              "securityGroupIDs": [
                "string"
              ],
              "runAfterLaunch": true,
              "networkInterface": "string",
              "mbRam": 0,
              "instanceType": "string",
              "subnetIDs": [
                "string"
              ],
              "coresPerCpu": 0,
              "staticIp": "string",
              "tags": [
                {
                  "key": "string",
                  "value": "string"
                }
              ],
              "securityGroupAction": "FROM_POLICY",
              "privateIPs": [
                "string"
              ],
              "tenancy": "SHARED",
              "computeLocationId": "string",
              "subnetsHostProject": "string",
              "logicalLocationId": "string",
              "networkAdapterType": "string",
              "byolOnDedicatedInstance": true,
              "placementGroup": "string",
              "disks": [
                {
                  "iops": 0,
                  "type": "COPY_ORIGIN",
                  "name": "string"
                }
              ],
              "privateIPAction": "CREATE_NEW",
              "staticIpAction": "EXISTING",
              "dedicatedHostIdentifier": "string",
              "useSharedRam": true
            }
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/17/2019
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "HIGH")]
    [OutputType([PSCustomObject])]
    Param(
        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$InstanceId = [System.Guid]::Empty,

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$BlueprintId = [System.Guid]::Empty,

        [Parameter(Mandatory = $true, ParameterSetName = "Blueprint", ValueFromPipeline = $true, Position = 0)]
        [ValidateNotNull()]
        [System.Collections.Hashtable]$Blueprint = @{},

        [Parameter(Mandatory = $true, ParameterSetName = "BlueprintFile")]
        [ValidateScript({
            Test-Path $_
        })]
        [System.String]$Path,

        [Parameter()]
        [ValidateSet("ALLOCATE", "DONT_ALLOCATE", "AS_SUBENT")]
        [System.String]$PublicIPAction,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]$MachineName,

        [Parameter(ParameterSetName = "AWS")]
        [System.Boolean]$RunAfterLaunch = $true,

        [Parameter(ParameterSetName = "VCENTER")]
        [ValidateNotNullOrEmpty()]
        [System.String]$NetworkInterface,

        [Parameter(ParameterSetName = "VCENTER")]
        [ValidateRange(0, 32)]
        [System.Int32]$CoresPerCpu,        

        [Parameter(ParameterSetName = "AWS")]
        [ValidateNotNull()]
        [System.Collections.Hashtable[]]$Tags = @(),

        [Parameter()]
        [ValidateSet("FROM_POLICY")]
        [System.String]$SecurityGroupAction = "FROM_POLICY",

        [Parameter()]
        [ValidateSet("SHARED", "DEDICATED", "HOST")]
        [System.String]$Tenancy,

        [Parameter(ParameterSetName = "VCENTER")]
        [ValidateNotNullOrEmpty()]
        [System.String]$ComputeLocationId,

        [Parameter(ParameterSetName = "GCP")]
        [ValidateNotNullOrEmpty()]
        [System.String]$SubnetsHostProject,

        [Parameter(ParameterSetName = "VCENTER")]
        [ValidateNotNullOrEmpty()]
        [System.String]$LogicalLocationId = "CLOUDENDURE",

        [Parameter(ParameterSetName = "AWS")]
        [System.Boolean]$BYOLOnDedicatedInstance = $false,

        [Parameter(ParameterSetName = "AWS")]
        [ValidateNotNull()]
        [System.Collections.Hashtable[]]$Disks,

        [Parameter()]
        [ValidateSet("CREATE_NEW", "COPY_ORIGIN", "CUSTOM_IP")]
        [System.String]$PrivateIPAction,

        [Parameter()]
        [ValidateSet("EXISTING", "DONT_CREATE", "CREATE_NEW", "IF_IN_ORIGIN")]
        [System.String]$StaticIPAction,

        [Parameter(ParameterSetName = "AWS")]
        [ValidateNotNullOrEmpty()]
        [System.String]$DedicatedHostIdentifier = "AUTO_PLACEMENT",

        [Parameter(ParameterSetName = "VCENTER")]
        [System.Boolean]$UseSharedRam,

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty,

        [Parameter()]
        [Switch]$PassThru,

        [Parameter()]
        [Switch]$Force
    )

    DynamicParam {

        # Create the dictionary
        [System.Management.Automation.RuntimeDefinedParameterDictionary]$RuntimeParameterDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary

        # Only generate the dynamic parameters if a blueprint doc wasn't specified
        if (-not $PSBoundParameters.ContainsKey("Blueprint") -and -not $PSBoundParameters.ContainsKey("Path"))
        {
            $DynSplat = @{}

            if (-not [System.String]::IsNullOrEmpty($Session)) {
                $DynSplat.Add("Session", $Session)
            }

            if ($ProjectId -ne $null -and $ProjectId -ne [System.Guid]::Empty)
            {
                $DynSplat.Add("ProjectId", $ProjectId)
            }

            [PSCustomObject]$TargetCloudRegion = Get-CETargetCloudRegion @DynSplat

            $InstanceTypes = $TargetCloudRegion.InstanceTypes
            $InstanceTypes += "COPY_ORIGIN"
            $InstanceTypes += "CUSTOM"

            New-DynamicParameter -Name "InstanceType" -Type ([System.String]) -ValidateSet $InstanceTypes -Mandatory -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null

            if ($PrivateIPAction -ieq "CUSTOM_IP")
            {
                New-DynamicParameter -Name "PrivateIPs" -Type ([System.String[]]) -ValidateNotNullOrEmpty -Mandatory -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
            }

            if ($StaticIPAction -ieq "EXISTING")
            {
                New-DynamicParameter -Name "StaticIP" -Type ([System.String]) -Mandatory -ValidateSet $TargetCloudRegion.StaticIPs -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
            }

            if ($TargetCloudRegion -ne "GENERIC")
            {
                $CloudName = Get-CECloud -Id $TargetCloudRegion.Cloud | Select-Object -ExpandProperty Name
            }
            else
            {
                $CloudName = $TargetCloudRegion
            }
            
            switch ($CloudName)
            {
                "AWS" {

                    if ($TargetCloudRegion.IAMRoles.Length -gt 0)
                    {
                        New-DynamicParameter -Name "IAMRole" -Type ([System.String]) -ParameterSets @("AWS") -ValidateSet $TargetCloudRegion.IAMRoles -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
                    }

                    if ($TargetCloudRegion.PlacementGroups.Length -gt 0)
                    {
                        New-DynamicParameter -Name "PlacementGroup" -Type ([System.String]) -ValidateSet ($TargetCloudRegion.PlacementGroups) -ParameterSets @("AWS") -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
                    }

                    if ($TargetCloudRegion.Subnets.Length -gt 0)
                    {
                        $SubnetSet = $TargetCloudRegion.Subnets | Where-Object {$_.SubnetId -ne $null } | Select-Object -ExpandProperty SubnetId
                        # Add default to allow user to specify the default subnet for the configured region
                        $SubnetSet += "Default"

                        New-DynamicParameter -Name "SubnetIDs" -Type ([System.String[]]) -ValidateSet $SubnetSet -ParameterSets @("AWS") -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
                    }

                    $Type = Import-UnboundParameterCode -PassThru
                    $Subnets = $Type.GetMethod("GetUnboundParameterValue").MakeGenericMethod([System.Object]).Invoke($Type, @($PSCmdlet, "SubnetIDs", -1))

                    # Get the first subnet Id
                    $Key = [System.String]::Empty

                    if ($Subnets -is [System.Array])
                    {
                        $Key = $Subnets[0]
                    }
                    elseif ($Subnets -is [System.String])
                    {
                        if (-not [System.String]::IsNullOrEmpty($Subnets))
                        {
                            $Key = $Subnets
                        }
                    }

                    # Attempt to get the first subnet from the target region object
                    $Subnet = $TargetCloudRegion.Subnets | Where-Object {$_.Name -ieq $Key -or $_.SubnetId -ieq $Key} | Select-Object -First 1 -ErrorAction SilentlyContinue

                    # If the subnet is "Default", you won't be able to select a security group, so a new one will be created
                    # Make sure there are security groups in this region and that we found a matching one
                    # Subnet will be null if user selected "Default"
                    if ($TargetCloudRegion.SecurityGroups -ne $null -and $TargetCloudRegion.SecurityGroups.Length -gt 0 -and $Subnet -ne $null)
                    {
                        # Get the network Id based on the selected subnet so we can get the right security groups as options
                        [System.String[]]$SGSet = $TargetCloudRegion.SecurityGroups | Where-Object {$_.NetworkId -ieq $Subnet.NetworkId} | Select-Object -ExpandProperty SecurityGroupId

                        New-DynamicParameter -Name "SecurityGroupIDs" -Type ([System.String[]]) -ParameterSets @("AWS") -ValidateSet $SGSet -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
                    }

                    break
                }
                "GCP" {

                    break
                }
                "Azure" {

                    break
                }
                "VCENTER" {
                    if ($TargetCloudRegion.scsiAdapterTypes.Length -gt 0)
                    {
                        New-DynamicParameter -Name "SCSIAdapterType" -Type ([System.String]) -ParameterSets @("VCENTER") -ValidateSet $TargetCloudRegion.scsiAdapterTypes -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
                    }

                    New-DynamicParameter -Name "Cpus" -Type ([System.Int32]) -ParameterSets @("VCENTER") -ValidateRange @(1, $TargetCloudRegion.maxCoresPerMachineCpu) -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null

                    if ($TargetCloudRegion.networkAdapterTypes.Length -gt 0)
                    {
                        New-DynamicParameter -Name "NetworkAdapterType" -Type ([System.String]) -ParameterSets @("VCENTER") -ValidateSet $TargetCloudRegion.networkAdapterTypes -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
                    }

                    New-DynamicParameter -Name "CoresPerCpu" -Type ([System.Int32]) -ParameterSets @("VCENTER") -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
                    
                    New-DynamicParameter -Name "MBRAM" -Type ([System.Int32]) -ParameterSets @("VCENTER") -ValidateSet $TargetCloudRegion.maxCoresPerMachineCpu -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
                    
                    break
                }
                "GENERIC" {

                    break
                }
                default {
                    throw "The cloud environment $CloudName is not supported by this cmdlet yet."
                    break
                }
            }
        }

        return $RuntimeParameterDictionary
    }

    Begin {      
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        $Splat = @{}

        if (-not [System.String]::IsNullOrEmpty($Session))
        {
            $Splat.Add("Session", $Session)
        }

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.DefaultProjectId            
        }

        $Splat.Add("ProjectId", $ProjectId)

        if ($BlueprintId -ne [System.Guid]::Empty)
        {
            # This will throw an exception if the blueprint can't be found
            [System.Collections.Hashtable]$ExistingBlueprint = Get-CEBlueprint -Id $BlueprintId @Splat | ConvertTo-Hashtable
        }
        else
        {
            [System.Collections.Hashtable]$ExistingBlueprint = Get-CEBlueprint -All @Splat | Where-Object {$_.machineId -eq $InstanceId} | Select-Object -First 1 | ConvertTo-Hashtable

            if ($ExistingBlueprint -eq $null)
            {
                throw "Could not find an existing blueprint for instance $InstanceId."
            }
        }

        $BlueprintObject = [PSCustomObject]@{}

        switch ($PSCmdlet.ParameterSetName)
        {
            "BlueprintFile" {
                $BlueprintObject = Get-Content -Path $Path -Raw | ConvertFrom-Json
                break
            }
            "Blueprint" {
                $BlueprintObject = [PSCustomObject]$Blueprint
                break
            }
            default {
                # This is going to take all of the parameters supplied, put them into a hash table, and then create a json
                # document that is the blueprint
                $Blueprint = Convert-ParametersToHashtable -Parameters (Get-Command -Name $PSCmdlet.MyInvocation.InvocationName).Parameters `
                    -ParameterSetName $PSCmdlet.ParameterSetName `
                    -RuntimeParameterDictionary $RuntimeParameterDictionary `
                    -BoundParameters $PSBoundParameters 

                $BlueprintObject = [PSCustomObject]$Blueprint
            }
        }

        # Merge the original and new blueprint
        [System.Collections.Hashtable]$NewBluePrint = Merge-HashTables -Source $ExistingBlueprint -Update ($BlueprintObject | ConvertTo-Hashtable)

        if ($NewBluePrint["StaticIPAction"] -ne "EXISTING") {
            $NewBluePrint["StaticIP"] = ""
        }

        if ($NewBluePrint["PrivateIPAction"] -ne "CUSTOM_IP") {
            $NewBluePrint["PrivateIPs"] = @()
        }

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.DefaultProjectId
        }

        [System.String]$UrlPath = "/projects/$ProjectId/blueprints/$($NewBluePrint["Id"])"

        $ConfirmMessage = "Are you sure you want to update the blueprint configuration?"

        $Body = ConvertTo-Json -InputObject $NewBluePrint

        $WhatIfDescription = "Updated blueprint to $Body"
        $ConfirmCaption = "Update Blueprint"

        if ($Force -or $PSCmdlet.ShouldProcess($WhatIfDescription, $ConfirmMessage, $ConfirmCaption))
        {
            Write-Verbose -Message "Updating blueprint to :`r`n$Body"
            
            try {
                $Result = Invoke-CERequest -Path $UrlPath -Body $Body -Method Patch -Session $Session

                Write-Verbose -Message "Blueprint successfully modified."

                if ($PassThru)
                {
                    Write-Output -InputObject $Result
                }
            }
            catch [Exception] {
                throw "There was an issue updating the blueprint: $($_.Exception.Message)"
            }
        }
    }

    End {
    }
}

#endregion
 
#region Replication Configuration

Function Get-CEMachineRecoveryPoints {
    <#
        .SYNOPSIS
            Returns the list of available recovery points for the specified machine.
 
        .DESCRIPTION
            Returns the list of available recovery points for the specified machine.
 
            This is only available if the license type is DR.
 
        .PARAMETER InstanceId
            The CE instance to retrieve recovery points in time information about.
 
        .PARAMETER Offset
            With which item to start (0 based).
 
        .PARAMETER Limit
            A number specifying how many entries to return between 0 and 1500 (defaults to 1500).
 
        .PARAMETER ProjectId
            The project Id to use to retrieve the details. This defaults to the current project retrieved from the login.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .EXAMPLE
            Get-CEMachineRecoveryPoints -InstanceId 47d842b8-ebfa-4695-90f8-fb9ab686c708
 
            This gets a list of the recovery points for the specified instance.
 
        .EXAMPLE
            Get-CEMachineRecoveryPoints -InstanceId 47d842b8-ebfa-4695-90f8-fb9ab686c708 -Offset 1501 -Limit 50
 
            This gets a list of the recovery points for the specified instance from index 1501 to 1551.
 
        .INPUTS
            System.Guid
 
        .OUTPUTS
           System.Management.Automation.PSCustomObject[]
             
            The JSON representation of the array:
            [
                {
                    "id": "string",
                    "dateTime": "2017-09-06T01:39:46Z"
                }
            ]
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/21/2019
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$InstanceId,

        [Parameter()]
        [ValidateRange(0, [System.UInt32]::MaxValue)]
        [System.UInt32]$Offset = 0,

        [Parameter()]
        [ValidateRange(0, 1500)]
        [System.UInt32]$Limit = 1500,

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }

    Process {

        $SessionInfo = Get-CESessionOrDefault -Session $Session
        
        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.DefaultProjectId
        }

        [System.String]$Path = "/projects/$ProjectId/machines/$InstanceId/pointsintime"

        if ($Offset -gt 0 -or $Limit -lt 1500)
        {
            $QueryString = [System.String]::Empty

            if ($Offset -gt 0)
            {
                $QueryString += "&offset=$Offset"
            }

            if ($Limit -lt 1500)
            {
                $QueryString += "&limit=$Limit"
            }

            # Remove the first character which is an unecessary ampersand
            $Path += "?$($QueryString.Substring(1))"
        }

        try {
            $Result = Invoke-CERequest -Path $Path -Method Get -Session $Session -ExpectedResponseCode 200
            Write-Output -InputObject $Result.Items
        }
        catch [Exception] {
            throw "There was an issue retrieving the recovery points: $($_.Exception.Message)"
        }            
    }

    End {
    }
}

Function Get-CEMachineBandwidth {
    <#
        .SYNOPSIS
            Returns the value of network bandwidth throttling setting for the specified machine.
 
        .DESCRIPTION
            Gets the setting in Mbps to use for replication. If this is set to 0, no throttling is applied.
 
        .PARAMETER InstanceId
            The CE instance to get the network bandwidth throttling setting for.
         
        .PARAMETER ProjectId
            The project Id to use to retrieve the details. This defaults to the current project retrieved from the login.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .EXAMPLE
            Get-CEMachineBandwidth -InstanceId 47d842b8-ebfa-4695-90f8-fb9ab686c708
 
            This the bandwidth throttling setting for the instance specified.
 
        .INPUTS
            System.Guid
 
        .OUTPUTS
           System.Int32
             
            The JSON representation of the returned object:
            {
                "bandwidthThrottling": 0
            }
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/21/2019
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$InstanceId,

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.DefaultProjectId
        }

        [System.String]$Path = "/projects/$ProjectId/machines/$InstanceId/bandwidthThrottling"

        try {
            $Result = Invoke-CERequest -Path $Path -Method Get -Session $Session -ExpectedResponseCode 200
            Write-Output -InputObject ($Result | Select-Object -ExpandProperty bandwidthThrottling)
        }
        catch [Exception] {
            throw "There was an issue retrieving bandwidth throttling setting: $($_.Exception.Message)"
        }
    }

    End {
    }
}

Function Set-CEMachineBandwidth {
    <#
        .SYNOPSIS
            Sets the value of the network bandwidth throttling setting for the specified machine.
 
        .DESCRIPTION
            The cmdlet sets or unsets the amount of bandwidth to be used for replication. The value is specified in Mbps. Specify a value of 0 to remove any
            existing throttling.
 
        .PARAMETER InstanceId
            The CE instance to set the network bandwidth throttling setting for.
 
        .PARAMETER BandwidthThrottling
            The value in Mbps to set for bandwidth throttling. A value of 0 removes any existing throttling.
         
        .PARAMETER ProjectId
            The project Id to use to set the configuration. This defaults to the current project retrieved from the login.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .EXAMPLE
            Set-CEMachineBandwidth -InstanceId 47d842b8-ebfa-4695-90f8-fb9ab686c708 -BandwidthThrottling 10
 
            This limits the amount of bandwidth to be used for replication to 10 Mbps for the specified instance.
 
        .EXAMPLE
            Set-CEMachineBandwidth -InstanceId 47d842b8-ebfa-4695-90f8-fb9ab686c708 -BandwidthThrottling 0
 
            This removes any throttling applied to the specified instance.
 
        .INPUTS
            None
 
        .OUTPUTS
           None or System.Int32
             
            The JSON representation of the returned object:
            {
                "bandwidthThrottling": 0
            }
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/21/2019
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$InstanceId,

        [Parameter(Mandatory = $true, Position = 1 )]
        [ValidateRange(0, [System.Int32]::MaxValue)]
        [System.Int32]$BandwidthThrottling,

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [Switch]$PassThru,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.DefaultProjectId
        }

        [System.String]$Path = "/projects/$ProjectId/machines/$InstanceId/bandwidthThrottling"
        [System.String]$Body = ConvertTo-Json -InputObject @{bandwidthThrottling = $BandwidthThrottling}


        Write-Verbose -Message "Sending updated setting of:`r`n$Body"

        try {
            $Result = Invoke-CERequest -Path $Path -Method Patch -Body $Body -Session $Session -ExpectedResponseCode 200

            if ($PassThru)
            {
                Write-Output -InputObject ($Result | Select-Object -ExpandProperty bandwidthThrottling)
            }
        }
        catch [Exception] {
            throw "There was an issue setting bandwidth throttling setting: $($_.Exception.Message)"
        }
    }

    End {

    }
}

Function New-CEReplicationConfiguration {
    <#
        .SYNOPSIS
            Creates a new CE replication configuration.
 
        .DESCRIPTION
            This cmdlet is used to create a new CE replication configuration for a specific CE account. If you provide a config object or file, it is important to note that the parameters are extremely case sensitive!
 
            Use the following formatting for parameter names in a file or config object:
 
            {
              "volumeEncryptionKey": "string",
              "replicationTags": [
                {
                  "key": "string",
                  "value": "string"
                }
              ],
              "disablePublicIp": true,
              "subnetHostProject": "string",
              "replicationServerType": "string",
              "useLowCostDisks": true,
              "computeLocationId": "string",
              "cloudCredentials": "string",
              "subnetId": "string",
              "logicalLocationId": "string",
              "bandwidthThrottling": 0,
              "useDedicatedServer": true,
              "zone": "string",
              "replicatorSecurityGroupIDs": [
                "string"
              ],
              "usePrivateIp": true,
              "region": "string",
              "id": "string",
              "proxyUrl": "string",
              "volumeEncryptionAllowed": true,
              "objectStorageLocation": "string",
              "archivingEnabled": true,
              "storageLocationId": "string"
            }
 
            You must also provide all parameters, and none of them can be null.
 
        .PARAMETER VolumeEncryptionKey
            AWS only. ARN to private key for volume encryption.
 
        .PARAMETER ReplicationTags
            AWS only. Tags that will be applied to every cloud resource created in the CloudEndure staging area.
 
        .PARAMETER DisablePublicIp
            When private IP is used, do not allocate public IP for replication server. Defaults to false.
 
        .PARAMETER SubnetHostProject
            GCP only. Host project of cross project network subnet.
 
        .PARAMETER ReplicationServerType
            The type of the replication server.
 
        .PARAMETER UseLowCostDisks
            Specify true to use low cost disks for eplication whenever possible.
 
        .PARAMETER ComputeLocationId
            VCENTER only.
 
        .PARAMETER CloudCredentials
            The ID for the cloudCredentials object containing the credentials to be used for accessing the target cloud. If this is not specified, the default credentials Id from the session will be used.
 
        .PARAMETER SubnetId
            Subnet where replication servers will be created.
 
        .PARAMETER LogicalLocationId
            VCENTER only. vcenter = vmFolder
 
        .PARAMETER BandwidthThrottling
            Mbps to use for Data Replication (zero means no throttling).
 
        .PARAMETER UseDedicatedServer
            This will dedicate a single Replication Server for each source machine, instead of a single Replication Server for multiple source machines.
 
        .PARAMETER Zone
            GCP and Azure ARM only. The zone to replicate into.
 
        .PARAMETER ReplicatorSecurityGroupIDs
            AWS only. The security groups that will be applied to the replication servers.
         
        .PARAMETER UsePrivateIp
            Should the CloudEndure agent access the replication server using its private IP address. Set this parameter to true to use a VPN, DirectConnect, ExpressRoute, or GCP Carrier Interconnect/Direct Peering.
 
        .PARAMETER ProxyUrl
            The full URI for a proxy (schema, username, password, domain, port) if required for the CloudEndure agent. Leave blank to not use a proxy.
 
        .PARAMETER VolumeEncryptionAllowed
            Specify if volume encryption is allowed.
 
        .PARAMETER ObjectStorageLocation
            AWS only. The bucket in AWS to store data.
 
        .PARAMETER ArchivingEnabled
            Is archiving enabled.
 
        .PARAMETER StorageLocationId
            The storage location id.
 
        .PARAMETER Config
            You can provide a replication config with these properties:
 
            {
              "volumeEncryptionKey": "string",
              "replicationTags": [
                {
                  "key": "string",
                  "value": "string"
                }
              ],
              "disablePublicIp": true,
              "subnetHostProject": "string",
              "replicationServerType": "string",
              "useLowCostDisks": true,
              "computeLocationId": "string",
              "cloudCredentials": "string",
              "subnetId": "string",
              "logicalLocationId": "string",
              "bandwidthThrottling": 0,
              "useDedicatedServer": true,
              "zone": "string",
              "replicatorSecurityGroupIDs": [
                "string"
              ],
              "usePrivateIp": true,
              "region": "string",
              "id": "string",
              "proxyUrl": "string",
              "volumeEncryptionAllowed": true,
              "objectStorageLocation": "string",
              "archivingEnabled": true,
              "storageLocationId": "string"
            }
 
            You cannot specify an updated Source as part of the config file, you must specify that separately.
 
        .PARAMETER Path
            The path to the replication configuration json file.
 
        .PARAMETER UpdateProject
            If specified, the project is automatically updated with this replication configuration.
 
        .PARAMETER SourceRegion
            The source region for the project. You only need to specify this if you specify the UpdateProject switch. You can separately update the project configuration with the source region.
 
        .PARAMETER TargetRegion
            The target region for the replication configuration.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .PARAMETER ProjectId
            The project Id to use to retrieve the details. This defaults to the current project retrieved from the login.
 
        .PARAMETER PassThru
            Specify to return the updated config to the pipeline.
 
        .EXAMPLE
            New-CEReplicationConfiguration -SubnetId "subnet-421d476c" -Target "us-east-1" -Source "Generic"
 
            Creates a new CE replication configuration to specify that replication will be sent to AWS US-East-1, replication servers should be deployed in subnet-421d476c, and the source is a generic location.
 
        .INPUTS
            None or System.Collections.Hashtable
 
        .OUTPUTS
           None or System.Management.Automation.PSCustomObject
 
            The JSON representation of the return value:
            {
              "volumeEncryptionKey": "string",
              "replicationTags": [
                {
                  "key": "string",
                  "value": "string"
                }
              ],
              "disablePublicIp": true,
              "subnetHostProject": "string",
              "replicationServerType": "string",
              "useLowCostDisks": true,
              "computeLocationId": "string",
              "cloudCredentials": "string",
              "subnetId": "string",
              "logicalLocationId": "string",
              "bandwidthThrottling": 0,
              "useDedicatedServer": true,
              "zone": "string",
              "replicatorSecurityGroupIDs": [
                "string"
              ],
              "usePrivateIp": true,
              "region": "string",
              "id": "string",
              "proxyUrl": "string",
              "volumeEncryptionAllowed": true,
              "objectStorageLocation": "string",
              "archivingEnabled": true,
              "storageLocationId": "string"
            }
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/21/2019
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "HIGH", DefaultParameterSetName = "__AllParameterSets")]
    [OutputType([PSCustomObject])]
    Param(
        [Parameter(ParameterSetName = "Config", Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [ValidateNotNull()]
        [System.Collections.Hashtable]$Config = @{},

        [Parameter(ParameterSetName = "Path", Mandatory = $true)]
        [ValidateScript({
            Test-Path -Path $_
        })]
        [System.String]$Path,

        [Parameter(ParameterSetName = "AWS")]
        [ValidateNotNull()]
        [System.Collections.Hashtable[]]$ReplicationTags = @(),

        [Parameter()]
        [System.Boolean]$DisablePublicIp = $false,

        [Parameter(ParameterSetName = "GCP")]
        [ValidateNotNullOrEmpty()]
        [System.String]$SubnetHostProject = [System.String]::Empty,

        [Parameter()]
        [System.Boolean]$UseLowCostDisks = $false,

        [Parameter(ParameterSetName = "VCENTER")]
        [ValidateNotNullOrEmpty()]
        [System.String]$ComputeLocationId = [System.String]::Empty,

        [Parameter()]
        [ValidateNotNull()]
        [System.Guid]$CloudCredentials = [System.Guid]::Empty,

        [Parameter(ParameterSetName = "VCENTER")]
        [ValidateNotNullOrEmpty()]
        [System.String]$LogicalLocationId = [System.String]::Empty,

        [Parameter()]
        [ValidateRange(0, [System.Int32]::MaxValue)]
        [System.Int32]$BandwidthThrottling = 0,

        [Parameter()]
        [System.Boolean]$UseDedicatedServer = $false,

        [Parameter(ParameterSetName = "GCP")]
        [Parameter(ParameterSetName = "Azure")]
        [ValidateNotNullOrEmpty()]
        [System.String]$Zone = [System.String]::Empty,

        [Parameter()]
        [System.Boolean]$UsePrivateIp = $false,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]$ProxyUrl = [System.String]::Empty,

        [Parameter()]
        [System.Boolean]$VolumeEncryptionAllowed = $false,

        [Parameter(ParameterSetName = "AWS")]
        [ValidateNotNullOrEmpty()]
        [System.String]$ObjectStorageLocation = [System.String]::Empty,

        [Parameter()]
        [System.Boolean]$ArchivingEnabled = $false,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]$StorageLocationId = [System.String]::Empty,

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [Switch]$UpdateProject,

        [Parameter()]
        [Switch]$PassThru,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    DynamicParam {

        # Create the dictionary
        [System.Management.Automation.RuntimeDefinedParameterDictionary]$RuntimeParameterDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary

        $DynSplat = @{}
        $ProjectSplat = @{}
        $SessionSplat = @{}

        if (-not [System.String]::IsNullOrEmpty($Session)) {
            $DynSplat.Add("Session", $Session)
            $ProjectSplat.Add("Session", $Session)
            $SessionSplat.Add("Session", $Session)
        }

        if ($ProjectId -ne $null -and $ProjectId -ne [System.Guid]::Empty)
        {
            $ProjectSplat.Add("Id", $ProjectId)
            $DynSplat.Add("ProjectId", $ProjectId)
        }

        [PSCustomObject]$Project = Get-CEProject @ProjectSplat
        [System.String]$TargetCloudId = $Project.TargetCloudId.Split(" ")[0] # For some reason PowerShell is duplicating the string content so you
        # end up with 2 Guids separated by a space

        if ($CloudCredentials -ne $null -and $CloudCredentials -ne [System.Guid]::Empty)
        {
            $DynSplat.Add("CloudCredentials", $CloudCredentials)
        }
        else
        {
            $DynSplat.Add("CloudCredentials", $Project.cloudCredentialsIDs[0])
        }
    
        $Regions = Get-CECloudRegion @DynSplat
        $TargetRegionsSet = $Regions | Select-Object -ExpandProperty Id
        New-DynamicParameter -Name "TargetRegion" -Type ([System.String]) -ValidateSet $TargetRegionsSet -Mandatory -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null

        $SourceRegionsSet = $Regions | Select-Object -ExpandProperty Id
        $SourceRegionsSet += "GENERIC"
        New-DynamicParameter -Name "SourceRegion" -Type ([System.String]) -ValidateSet $SourceRegionsSet -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null

        # Only generate the rest of the dynamic parameters if a replication config wasn't specified
        if (-not $PSBoundParameters.ContainsKey("Config") -and -not $PSBoundParameters.ContainsKey("Path") -and $Config -ne @{})
        {            
            $Type = Import-UnboundParameterCode -PassThru

            $TargetRegion = $Type.GetMethod("GetUnboundParameterValue").MakeGenericMethod([System.String]).Invoke($Type, @($PSCmdlet, "TargetRegion", -1))

            if (-not [System.String]::IsNullOrEmpty($TargetRegion))
            {
                $TargetCloudRegion = Get-CECloudRegion -Id $TargetRegion @DynSplat

                if ($TargetCloudRegion.Subnets.Length -gt 0)
                {
                    $SubnetSet = $TargetCloudRegion.Subnets | Where-Object {$_.SubnetId -ne $null } | Select-Object -ExpandProperty SubnetId
                    # Add default to allow user to specify the default subnet for the configured region
                    $SubnetSet += "Default"

                    New-DynamicParameter -Name "SubnetId" -Type ([System.String]) -ValidateSet $SubnetSet -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
                }

                $InstanceTypes = $TargetCloudRegion.InstanceTypes
                $InstanceTypes += "Default"
                New-DynamicParameter -Name "ReplicationServerType" -Type ([System.String]) -ValidateSet $InstanceTypes -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
            }

            $CloudName = Get-CECloud -Id $TargetCloudId @SessionSplat | Select-Object -ExpandProperty Name

            switch ($CloudName)
            {
                "AWS" {
                    # Creds may not have access to opt-in regions that get listed
                    if (Get-Member -InputObject $TargetCloudRegion -MemberType Properties -Name "VolumeEncryptionKeys")
                    {
                        $KeySet = $TargetCloudRegion | Select-Object -ExpandProperty VolumeEncryptionKeys | Where-Object {$_.KeyArn -ne $null} | Select-Object -ExpandProperty KeyArn
                        $KeySet += "Default"

                        New-DynamicParameter -Name "VolumeEncryptionKey" -Type ([System.String]) -ValidateSet $KeySet -ParameterSets @("AWS") -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
                    }

                    [System.String]$Key = $Type.GetMethod("GetUnboundParameterValue").MakeGenericMethod([System.String]).Invoke($Type, @($PSCmdlet, "SubnetId", -1))

                    if ([System.String]::IsNullOrEmpty($Key))
                    {
                        $Key = "Default"
                    }

                    # The target region may not have been specified yet
                    if ($TargetCloudRegion -ne $null)
                    {
                        $Subnet = $TargetCloudRegion.Subnets | Where-Object {$_.SubnetId -ieq $Key} | Select-Object -First 1 -ErrorAction SilentlyContinue
    
                        # If the subnet is "Default", you won't be able to select a security group, so a new one will be created
                        # Make sure there are security groups in this region and that we found a matching one
                        # Subnet will be null if user selected "Default"
                        if ($TargetCloudRegion.SecurityGroups -ne $null -and $TargetCloudRegion.SecurityGroups.Length -gt 0 -and $Subnet -ne $null -and $Subnet.Name -ine "Default")
                        {
                            # Get the network Id based on the selected subnet so we can get the right security groups as options
                            [System.String[]]$SGSet = $TargetCloudRegion.SecurityGroups | Where-Object {$_.NetworkId -ieq $Subnet.NetworkId} | Select-Object -ExpandProperty SecurityGroupId

                            New-DynamicParameter -Name "ReplicatorSecurityGroupIDs" -Type ([System.String[]]) -ParameterSets @("AWS") -ValidateSet $SGSet -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
                        }
                    }

                    break
                }
                "GCP" {
                    if ($TargetCloudRegion -ne $null -and $TargetCloudRegion.Zones.Length -gt 0)
                    {
                        $ZoneSet = $TargetCloudRegion.Zones
                        # Add default to allow user to specify the default zone for the configured region
                        $ZoneSet += "Default"

                        New-DynamicParameter -Name "Zone" -Type ([System.String]) -ValidateSet $ZoneSet -ParameterSets @("GCP") -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
                    }
                    break
                }
                "Azure" {
                    if ($TargetCloudRegion -ne $null -and $TargetCloudRegion.Zones.Length -gt 0)
                    {
                        $ZoneSet = $TargetCloudRegion.Zones
                        # Add default to allow user to specify the default zone for the configured region
                        $ZoneSet += "Default"

                        New-DynamicParameter -Name "Zone" -Type ([System.String]) -ValidateSet $ZoneSet -ParameterSets @("Azure") -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
                    }
                    break
                }
            }
        }

        return $RuntimeParameterDictionary
    }

    Begin {        
    }

    Process {        
        $SessionInfo = Get-CESessionOrDefault -Session $Session
        
        $ReplicationObject = @{
            region = "";
            volumeEncryptionKey = "";
            subnetId = "";
            zone = "";
            usePrivateIp = $false;
            disablePublicIp = $false;
            useDedicatedServer = $false;
            replicationServerType = "Default";
            replicationTags = @();
            replicatorSecurityGroupIDs = @();
            volumeEncryptionAllowed = $false;
            storageLocationId = "";
            useLowCostDisks = $false;
            proxyUrl = "";
            bandwidthThrottling = 0;            
        }

        #$ReplicationObject = [PSCustomObject]@{}
        
        switch ($PSCmdlet.ParameterSetName)
        {
            "Path" {
                #$ReplicationObject = Get-Content -Path $Path -Raw | ConvertFrom-Json
                $Temp = Get-Content -Path $Path -Raw | ConvertFrom-Json
                
                
                foreach ($Key in ($Temp | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name))
                {
                    if ($ReplicationObject.ContainsKey($Key))
                    {
                        $ReplicationObject[$Key] = $Temp.$Key
                    }
                    else
                    {
                        $ReplicationObject.Add($Key, $Temp.$Key)
                    }
                }
                
                break
            }
            "Config" {
                
                foreach ($Item in $Config.GetEnumerator())
                {
                    if ($ReplicationObject.ContainsKey($Item.Key))
                    {
                        $ReplicationObject[$Item.Key] = $Item.Value
                    }
                    else
                    {
                        $ReplicationObject.Add($Item.Key, $Item.Value)
                    }
                }
                
                #$ReplicationObject = [PSCustomObject]$Config

                break
            }
            default {
                # This is going to take all of the parameters supplied, put them into a hash table, and then create a json
                # document that is the blueprint
                $Config = Convert-ParametersToHashtable -Parameters (Get-Command -Name $PSCmdlet.MyInvocation.InvocationName).Parameters `
                    -ParameterSetName $script:AllParameterSets `
                    -RuntimeParameterDictionary $RuntimeParameterDictionary `
                    -BoundParameters $PSBoundParameters `
                    -FunctionName $PSCmdlet.MyInvocation.InvocationName 

                
                foreach ($Item in $Config.GetEnumerator())
                {
                    if ($ReplicationObject.ContainsKey($Item.Key))
                    {
                        $ReplicationObject[$Item.Key] = $Item.Value
                    }
                    else
                    {
                        $ReplicationObject.Add($Item.Key, $Item.Value)
                    }
                }

                if ($ReplicationObject.ContainsKey("sourceRegion"))
                {
                    $ReplicationObject.Remove("sourceRegion")
                }

                if ($ReplicationObject.ContainsKey("targetRegion"))
                {
                    $ReplicationObject.Remove("targetRegion")
                }

                if ($ReplicationObject.ContainsKey("updateProject"))
                {
                    $ReplicationObject.Remove("updateProject")
                }
                
                <#
                if ($Config.ContainsKey("sourceRegion"))
                {
                    $Config.Remove("sourceRegion")
                }
 
                if ($Config.ContainsKey("targetRegion"))
                {
                    $Config.Remove("targetRegion")
                }
 
                $ReplicationObject = [PSCustomObject]$Config
                #>


                break
            }
        }

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.DefaultProjectId
        }

        $Splat = @{}

        if (-not [System.String]::IsNullOrEmpty($Session))
        {
            $Splat.Add("Session", $Session)
        }

        $Project = Get-CEProject -Id $ProjectId @Splat

        <#
        if (($ReplicationObject | Get-Member -MemberType Properties -Name cloudCredentials) -and [System.String]::IsNullOrEmpty($ReplicationObject.cloudCredentials))
        {
            $ReplicationObject.cloudCredentials = $Project.CloudCredentialsIDs[0]
        }
 
        if (($ReplicationObject | Get-Member -MemberType Properties -Name subnetId) -and $ReplicationObject.subnetId -ieq "Default")
        {
            $ReplicationObject.subnetId = ""
        }
        #>


        if ($ReplicationObject.ContainsKey("cloudCredentials") -and  [System.String]::IsNullOrEmpty($ReplicationObject["cloudCredentials"]))
        {
            $ReplicationObject["cloudCredentials"] = $Project.CloudCredentialsIDs[0]
        }

        if ($ReplicationObject.ContainsKey("subnetId") -and $ReplicationObject["subnetId"] -ieq "default")
        {
            $ReplicationObject["subnetId"] = ""
        }

        # Must specify the region for the target since this won't be set yet in the project
        # If the project has a target region, it's because it has an associated replication
        # configuration, which means we wouldn't be creating a new one
        if (-not $ReplicationObject.ContainsKey("region"))
        {
            $ReplicationObject.Add("region", $PSBoundParameters["TargetRegion"])
        }
        elseif ([System.String]::IsNullOrEmpty($ReplicationObject["region"]))
        {
            $ReplicationObject["region"] = $PSBoundParameters["TargetRegion"]
        }

        <#
        if (-not ($ReplicationObject | Get-Member -MemberType Properties -Name region))
        {
            $ReplicationObject | Add-Member -MemberType NoteProperty -Name region -Value $PSBoundParameters["TargetRegion"]
        }
        #>

        
        $Body = ConvertTo-Json -InputObject $ReplicationObject -Compress
        Write-Verbose -Message "Sending new config:`n$Body"

        [System.String]$UrlPath = "/projects/$ProjectId/replicationConfigurations"

        $ErrorHandling = {
            Param($StatusCode, $Content, $ErrorMessage)
            
            switch ($StatusCode) {
                201 {    
                    Write-Verbose -Message $Content
                    Write-Output -InputObject (ConvertFrom-Json -InputObject $Content)
                    break
                }
                400 {
                    throw "There is a conflict in the replication configuration. This can be due to: subnet ID which does not exist in the region, security groups that are not in the same network as the subnet, etc."
                }
                default {
                    # Make sure we don't send the patch request if this failed
                    throw "Failed to create new Replication Configuration with error: $ErrorMessage"
                }
            }
        }

        $Result = Invoke-CERequest -Path $UrlPath -Method Post -Body $Body -Session $Session -ErrorHandling $ErrorHandling

        if ($PassThru)
        {
            Write-Output -InputObject $Result
        }

        # After creating the new replicuation configuration, we need to patch the project
        # with the replication configuration id and source region id

        if ($UpdateProject)
        {
            $Splat = @{}
            
            if ($PSBoundParameters.ContainsKey("SourceRegion"))
            {
                if ($PSBoundParameters["SourceRegion"] -ieq "GENERIC")
                {
                    $Splat.Add("Source", "Generic")
                }
                else
                {
                    $Splat.Add("SourceId", $PSBoundParameters["SourceRegion"])
                }
            }

            if (-not [System.String]::IsNullOrEmpty($Session))
            {
                $Splat.Add("Session", $Session)
            }

            if (-not [System.String]::IsNullOrEmpty($ProjectId))
            {
                $Splat.Add("ProjectId", $ProjectId)
            }

            try {
                Set-CEProject -ReplicationConfiguration $Result.Id @Splat -Force
                Write-Verbose -Message "Successfully updated project."
            }
            catch [Exception] {
                throw "Could not update project with replication configuration: $($_.Exception.Message)"
            }
        }
    }

    End {
    }
}

Function Get-CEReplicationConfiguration {
    <#
        .SYNOPSIS
           Gets the replication configuration.
 
        .DESCRIPTION
            The cmdlet retrieves information about the replication configuration.
 
        .PARAMETER Id
            The id of the replication configuration to retrieve. If this is not specified, all replication configurations will be returned.
 
        .PARAMETER Offset
            With which item to start (0 based).
 
        .PARAMETER Limit
            A number specifying how many entries to return between 0 and 1500 (defaults to 1500).
 
        .PARAMETER ProjectId
            The project Id to use to retrieve the details. This defaults to the current project retrieved from the login.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
             
        .EXAMPLE
            Get-CEReplicationConfiguration
 
            Retrieves the replication configuration of the current account.
 
        .INPUTS
            None or System.Guid
 
        .OUTPUTS
            System.Management.Automation.PSCustomObject or System.Management.Automation.PSCustomObject[]
 
            The JSON representation of the array:
            [
                {
                  "volumeEncryptionKey": "string",
                  "replicationTags": [
                    {
                      "key": "string",
                      "value": "string"
                    }
                  ],
                  "disablePublicIp": true,
                  "subnetHostProject": "string",
                  "replicationServerType": "string",
                  "useLowCostDisks": true,
                  "computeLocationId": "string",
                  "cloudCredentials": "string",
                  "subnetId": "string",
                  "logicalLocationId": "string",
                  "bandwidthThrottling": 0,
                  "useDedicatedServer": true,
                  "zone": "string",
                  "replicatorSecurityGroupIDs": [
                    "string"
                  ],
                  "usePrivateIp": true,
                  "region": "string",
                  "id": "string",
                  "proxyUrl": "string",
                  "volumeEncryptionAllowed": true,
                  "objectStorageLocation": "string",
                  "archivingEnabled": true,
                  "storageLocationId": "string"
                }
            ]
             
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/21/2019
    #>

    [CmdletBinding(DefaultParameterSetName = "List")]
    [OutputType([PSCustomObject], [PSCustomObject[]])]
    Param(
        [Parameter(ValueFromPipeline = $true, Mandatory = $true, ParameterSetName="GetById")]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$Id = [System.Guid]::Empty,

        [Parameter(ParameterSetName = "List")]
        [ValidateRange(0, [System.UInt32]::MaxValue)]
        [System.UInt32]$Offset = 0,

        [Parameter(ParameterSetName = "List")]
        [ValidateRange(0, 1500)]
        [System.UInt32]$Limit = 1500,

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {        
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo["DefaultProjectId"]
        }

        [System.String]$UrlPath = "/projects/$ProjectId/replicationConfigurations"

        [System.Int32]$ResultCount = 0
        [System.Boolean]$Found = $false

        do {
            [System.String]$QueryString = "?offset=$Offset&limit=$Limit"
            [System.String]$TempUri = "$UrlPath$QueryString"

            Write-Verbose -Message "Querying replication configurations from $Offset to $($Offset + $Limit)."

            try {
                $Result = Invoke-CERequest -Path $TempUri -Method Get -Session $Session -ExpectedResponseCode 200
            }
            catch [Exception] {
                throw "There was an issue listing the replication configurations: $($_.Exception.Message)"
            }

            [PSCustomObject[]]$Content = $Result.Items
            $ResultCount = $Result.Items.Length

            switch -wildcard ($PSCmdlet.ParameterSetName)
            {
                "Get*" {

                    $Filter = {$_.Id -ieq $Id.ToString()}

                    $ReplConfig = $Content | Where-Object $Filter

                    if ($ReplConfig -ne $null)
                    {
                        Write-Output -InputObject ([PSCustomObject]($ReplConfig | Select-Object -First 1))
                        $Found = $true
                        break
                    }
                    else
                    {
                        $Offset += $Limit
                    }

                    break
                }
                "List" {
                    Write-Output -InputObject $Content
                    $ResultCount = $Limit - 1 # Make sure we break the do loop
                    break
                }
                default {
                    throw "Encountered an unknown parameter set $($PSCmdlet.ParameterSetName)."
                }
            }

        } while ($ResultCount -ge $Limit)

        if ($PSCmdlet.ParameterSetName -like "Get*" -and -not $Found)
        {
            throw "The replication configuration with Id $Id was not found."
        }
    }

    End {
    }
}

Function Set-CEReplicationConfiguration {
    <#
        .SYNOPSIS
            Updates a CE replication configuration.
 
        .DESCRIPTION
            This cmdlet is used to update the CE replication configuration for a specific CE project. If you provide a config object or file, it is important to note that the parameters are extremely case sensitive!
 
            Modifying volumeEncryptionKey or modifying cloudCredentials to ones matching a different cloud account will result in replication restarting from initial sync.
 
            Use the following formatting for parameter names in a file or config object:
 
            {
              "volumeEncryptionKey": "string",
              "replicationTags": [
                {
                  "key": "string",
                  "value": "string"
                }
              ],
              "disablePublicIp": true,
              "subnetHostProject": "string",
              "replicationServerType": "string",
              "useLowCostDisks": true,
              "computeLocationId": "string",
              "cloudCredentials": "string",
              "subnetId": "string",
              "logicalLocationId": "string",
              "bandwidthThrottling": 0,
              "useDedicatedServer": true,
              "zone": "string",
              "replicatorSecurityGroupIDs": [
                "string"
              ],
              "usePrivateIp": true,
              "region": "string",
              "id": "string",
              "proxyUrl": "string",
              "volumeEncryptionAllowed": true,
              "objectStorageLocation": "string",
              "archivingEnabled": true,
              "storageLocationId": "string"
            }
 
            Only supply the parameters you want to update.
 
        .PARAMETER VolumeEncryptionKey
            AWS only. ARN to private key for volume encryption.
 
        .PARAMETER ReplicationTags
            AWS only. Tags that will be applied to every cloud resource created in the CloudEndure staging area.
 
        .PARAMETER DisablePublicIp
            When private IP is used, do not allocate public IP for replication server. Defaults to false.
 
        .PARAMETER SubnetHostProject
            GCP only. Host project of cross project network subnet.
 
        .PARAMETER ReplicationServerType
            The type of the replication server.
 
        .PARAMETER UseLowCostDisks
            Specify true to use low cost disks for eplication whenever possible.
 
        .PARAMETER ComputeLocationId
            VCENTER only.
 
        .PARAMETER CloudCredentials
            The ID for the cloudCredentials object containing the credentials to be used for accessing the target cloud. If this is not specified, the default credentials Id from the session will be used.
 
        .PARAMETER SubnetId
            Subnet where replication servers will be created.
 
        .PARAMETER LogicalLocationId
            VCENTER only. vcenter = vmFolder
 
        .PARAMETER BandwidthThrottling
            Mbps to use for Data Replication (zero means no throttling).
 
        .PARAMETER UseDedicatedServer
            This will dedicate a single Replication Server for each source machine, instead of a single Replication Server for multiple source machines.
 
        .PARAMETER Zone
            GCP and Azure ARM only. The zone to replicate into.
 
        .PARAMETER ReplicatorSecurityGroupIDs
            AWS only. The security groups that will be applied to the replication servers.
         
        .PARAMETER UsePrivateIp
            Should the CloudEndure agent access the replication server using its private IP address. Set this parameter to true to use a VPN, DirectConnect, ExpressRoute, or GCP Carrier Interconnect/Direct Peering.
 
        .PARAMETER ProxyUrl
            The full URI for a proxy (schema, username, password, domain, port) if required for the CloudEndure agent. Leave blank to not use a proxy.
 
        .PARAMETER VolumeEncryptionAllowed
            Specify if volume encryption is allowed.
 
        .PARAMETER ObjectStorageLocation
            AWS only. The bucket in AWS to store data.
 
        .PARAMETER ArchivingEnabled
            Is archiving enabled.
 
        .PARAMETER StorageLocationId
            The storage location id.
 
        .PARAMETER Config
            You can provide a replication config with these properties:
 
            {
              "volumeEncryptionKey": "string",
              "replicationTags": [
                {
                  "key": "string",
                  "value": "string"
                }
              ],
              "disablePublicIp": true,
              "subnetHostProject": "string",
              "replicationServerType": "string",
              "useLowCostDisks": true,
              "computeLocationId": "string",
              "cloudCredentials": "string",
              "subnetId": "string",
              "logicalLocationId": "string",
              "bandwidthThrottling": 0,
              "useDedicatedServer": true,
              "zone": "string",
              "replicatorSecurityGroupIDs": [
                "string"
              ],
              "usePrivateIp": true,
              "region": "string",
              "id": "string",
              "proxyUrl": "string",
              "volumeEncryptionAllowed": true,
              "objectStorageLocation": "string",
              "archivingEnabled": true,
              "storageLocationId": "string"
            }
 
            You cannot specify an updated Source or Target as part of the config file, you must specify that separately.
 
        .PARAMETER SourceRegion
            The source region for the project. You only need to specify this if you specify the UpdateProject switch. You can separately update the project configuration with the source region.
 
        .PARAMETER TargetRegion
            The target region for the replication configuration.
 
        .PARAMETER Path
            The path to the replication configuration json file.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .PARAMETER ProjectId
            The project Id to use to retrieve the details. This defaults to the current project retrieved from the login.
 
        .PARAMETER PassThru
            Specify to return the updated config to the pipeline.
 
        .EXAMPLE
            New-CEReplicationConfiguration -SubnetId "subnet-421d476c" -Target "us-east-1" -Source "Generic"
 
            Creates a new CE replication configuration to specify that replication will be sent to AWS US-East-1, replication servers should be deployed in subnet-421d476c, and the source is a generic location.
 
        .INPUTS
            None or System.Collections.Hashtable
 
        .OUTPUTS
           None or System.Management.Automation.PSCustomObject
 
            The JSON representation of the return value:
            {
              "volumeEncryptionKey": "string",
              "replicationTags": [
                {
                  "key": "string",
                  "value": "string"
                }
              ],
              "disablePublicIp": true,
              "subnetHostProject": "string",
              "replicationServerType": "string",
              "useLowCostDisks": true,
              "computeLocationId": "string",
              "cloudCredentials": "string",
              "subnetId": "string",
              "logicalLocationId": "string",
              "bandwidthThrottling": 0,
              "useDedicatedServer": true,
              "zone": "string",
              "replicatorSecurityGroupIDs": [
                "string"
              ],
              "usePrivateIp": true,
              "region": "string",
              "id": "string",
              "proxyUrl": "string",
              "volumeEncryptionAllowed": true,
              "objectStorageLocation": "string",
              "archivingEnabled": true,
              "storageLocationId": "string"
            }
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 11/21/2019
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "HIGH", DefaultParameterSetName = "__AllParameterSets")]
    [OutputType([PSCustomObject])]
    Param(
        [Parameter(Mandatory = $true)]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$Id,
        
        [Parameter(ParameterSetName = "Config", Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [ValidateNotNull()]
        [System.Collections.Hashtable]$Config = @{},

        [Parameter(ParameterSetName = "Path", Mandatory = $true)]
        [ValidateScript({
            Test-Path -Path $_
        })]
        [System.String]$Path,

        [Parameter(ParameterSetName = "AWS")]
        [ValidateNotNull()]
        [System.Collections.Hashtable[]]$ReplicationTags = @(),

        [Parameter()]
        [System.Boolean]$DisablePublicIp = $false,

        [Parameter(ParameterSetName = "GCP")]
        [ValidateNotNullOrEmpty()]
        [System.String]$SubnetHostProject = [System.String]::Empty,

        [Parameter()]
        [System.Boolean]$UseLowCostDisks = $false,

        [Parameter(ParameterSetName = "VCENTER")]
        [ValidateNotNullOrEmpty()]
        [System.String]$ComputeLocationId = [System.String]::Empty,

        [Parameter()]
        [ValidateNotNull()]
        [System.Guid]$CloudCredentials = [System.Guid]::Empty,

        [Parameter(ParameterSetName = "VCENTER")]
        [ValidateNotNullOrEmpty()]
        [System.String]$LogicalLocationId = [System.String]::Empty,

        [Parameter()]
        [ValidateRange(0, [System.Int32]::MaxValue)]
        [System.Int32]$BandwidthThrottling = 0,

        [Parameter()]
        [System.Boolean]$UseDedicatedServer = $false,

        [Parameter(ParameterSetName = "GCP")]
        [Parameter(ParameterSetName = "Azure")]
        [ValidateNotNullOrEmpty()]
        [System.String]$Zone = [System.String]::Empty,

        [Parameter()]
        [System.Boolean]$UsePrivateIp = $false,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]$ProxyUrl = [System.String]::Empty,

        [Parameter()]
        [System.Boolean]$VolumeEncryptionAllowed = $false,

        [Parameter(ParameterSetName = "AWS")]
        [ValidateNotNullOrEmpty()]
        [System.String]$ObjectStorageLocation = [System.String]::Empty,

        [Parameter()]
        [System.Boolean]$ArchivingEnabled = $false,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]$StorageLocationId = [System.String]::Empty,

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [Switch]$PassThru,

        [Parameter()]
        [Switch]$Force,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    DynamicParam {

        # Create the dictionary
        [System.Management.Automation.RuntimeDefinedParameterDictionary]$RuntimeParameterDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary

        $DynSplat = @{}
        $ProjectSplat = @{}
        $SessionSplat = @{}

        if (-not [System.String]::IsNullOrEmpty($Session)) {
            $DynSplat.Add("Session", $Session)
            $ProjectSplat.Add("Session", $Session)
            $SessionSplat.Add("Session", $Session)
        }

        if ($ProjectId -ne $null -and $ProjectId -ne [System.Guid]::Empty)
        {
            $ProjectSplat.Add("Id", $ProjectId)
            $DynSplat.Add("ProjectId", $ProjectId)
        }

        [PSCustomObject]$Project = Get-CEProject @ProjectSplat
        [System.String]$TargetCloudId = $Project.TargetCloudId.Split(" ")[0] # For some reason PowerShell is duplicating the string content so you
        # end up with 2 Guids separated by a space

        if ($CloudCredentials -ne $null -and $CloudCredentials -ne [System.Guid]::Empty)
        {
            $DynSplat.Add("CloudCredentials", $CloudCredentials)
        }
        else
        {
            $DynSplat.Add("CloudCredentials", $Project.cloudCredentialsIDs[0])
        }
    
        $Regions = Get-CECloudRegion @DynSplat
        $TargetRegionsSet = $Regions | Select-Object -ExpandProperty Id
        New-DynamicParameter -Name "TargetRegion" -Type ([System.String]) -ValidateSet $TargetRegionsSet -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null

        # Only generate the rest of the dynamic parameters if a replication config wasn't specified
        if (-not $PSBoundParameters.ContainsKey("Config") -and -not $PSBoundParameters.ContainsKey("Path") -and $Config -ne @{})
        {            
            $Type = Import-UnboundParameterCode -PassThru

            $TargetRegion = $Type.GetMethod("GetUnboundParameterValue").MakeGenericMethod([System.String]).Invoke($Type, @($PSCmdlet, "TargetRegion", -1))

            if (-not [System.String]::IsNullOrEmpty($TargetRegion))
            {
                $TargetCloudRegion = Get-CECloudRegion -Id $TargetRegion @DynSplat

                if ($TargetCloudRegion.Subnets.Length -gt 0)
                {
                    $SubnetSet = $TargetCloudRegion.Subnets | Where-Object {$_.SubnetId -ne $null } | Select-Object -ExpandProperty SubnetId
                    # Add default to allow user to specify the default subnet for the configured region
                    $SubnetSet += "Default"

                    New-DynamicParameter -Name "SubnetId" -Type ([System.String]) -ValidateSet $SubnetSet -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
                }

                $InstanceTypes = $TargetCloudRegion.InstanceTypes
                $InstanceTypes += "Default"
                New-DynamicParameter -Name "ReplicationServerType" -Type ([System.String]) -ValidateSet $InstanceTypes -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
            }

            $CloudName = Get-CECloud -Id $TargetCloudId @SessionSplat | Select-Object -ExpandProperty Name

            switch ($CloudName)
            {
                "AWS" {
                    # Creds may not have access to opt-in regions that get listed
                    if (Get-Member -InputObject $TargetCloudRegion -MemberType Properties -Name "VolumeEncryptionKeys")
                    {
                        $KeySet = $TargetCloudRegion | Select-Object -ExpandProperty VolumeEncryptionKeys | Where-Object {$_.KeyArn -ne $null} | Select-Object -ExpandProperty KeyArn
                        $KeySet += "Default"

                        New-DynamicParameter -Name "VolumeEncryptionKey" -Type ([System.String]) -ValidateSet $KeySet -ParameterSets @("AWS") -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
                    }

                    [System.String]$Key = $Type.GetMethod("GetUnboundParameterValue").MakeGenericMethod([System.String]).Invoke($Type, @($PSCmdlet, "SubnetId", -1))

                    if ([System.String]::IsNullOrEmpty($Key))
                    {
                        $Key = "Default"
                    }

                    # The target region may not have been specified yet
                    if ($TargetCloudRegion -ne $null)
                    {
                        $Subnet = $TargetCloudRegion.Subnets | Where-Object {$_.SubnetId -ieq $Key} | Select-Object -First 1 -ErrorAction SilentlyContinue
    
                        # If the subnet is "Default", you won't be able to select a security group, so a new one will be created
                        # Make sure there are security groups in this region and that we found a matching one
                        # Subnet will be null if user selected "Default"
                        if ($TargetCloudRegion.SecurityGroups -ne $null -and $TargetCloudRegion.SecurityGroups.Length -gt 0 -and $Subnet -ne $null -and $Subnet.Name -ine "Default")
                        {
                            # Get the network Id based on the selected subnet so we can get the right security groups as options
                            [System.String[]]$SGSet = $TargetCloudRegion.SecurityGroups | Where-Object {$_.NetworkId -ieq $Subnet.NetworkId} | Select-Object -ExpandProperty SecurityGroupId

                            New-DynamicParameter -Name "ReplicatorSecurityGroupIDs" -Type ([System.String[]]) -ParameterSets @("AWS") -ValidateSet $SGSet -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
                        }
                    }

                    break
                }
                "GCP" {
                    if ($TargetCloudRegion -ne $null -and $TargetCloudRegion.Zones.Length -gt 0)
                    {
                        $ZoneSet = $TargetCloudRegion.Zones
                        # Add default to allow user to specify the default zone for the configured region
                        $ZoneSet += "Default"

                        New-DynamicParameter -Name "Zone" -Type ([System.String]) -ValidateSet $ZoneSet -ParameterSets @("GCP") -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
                    }
                    break
                }
                "Azure" {
                    if ($TargetCloudRegion -ne $null -and $TargetCloudRegion.Zones.Length -gt 0)
                    {
                        $ZoneSet = $TargetCloudRegion.Zones
                        # Add default to allow user to specify the default zone for the configured region
                        $ZoneSet += "Default"

                        New-DynamicParameter -Name "Zone" -Type ([System.String]) -ValidateSet $ZoneSet -ParameterSets @("Azure") -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
                    }
                    break
                }
            }
        }

        return $RuntimeParameterDictionary
    }

    Begin {        
    }

    Process {        
        $SessionInfo = Get-CESessionOrDefault -Session $Session
        
        $ReplicationObject = @{}
        
        switch ($PSCmdlet.ParameterSetName)
        {
            "Path" {
                #$ReplicationObject = Get-Content -Path $Path -Raw | ConvertFrom-Json
                $Temp = Get-Content -Path $Path -Raw | ConvertFrom-Json
                            
                foreach ($Key in ($Temp | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name))
                {
                    $ReplicationObject.Add($Key, $Temp.$Key)
                }
                
                break
            }
            "Config" {
                
                foreach ($Item in $Config.GetEnumerator())
                {
                    $ReplicationObject.Add($Item.Key, $Item.Value)
                }
                
                break
            }
            default {
                # This is going to take all of the parameters supplied, put them into a hash table, and then create a json
                # document that is the blueprint
                $Config = Convert-ParametersToHashtable -Parameters (Get-Command -Name $PSCmdlet.MyInvocation.InvocationName).Parameters `
                    -ParameterSetName $script:AllParameterSets `
                    -RuntimeParameterDictionary $RuntimeParameterDictionary `
                    -BoundParameters $PSBoundParameters `
                    -FunctionName $PSCmdlet.MyInvocation.InvocationName 

                foreach ($Item in $Config.GetEnumerator())
                {
                    $ReplicationObject.Add($Item.Key, $Item.Value)
                }

                if ($ReplicationObject.ContainsKey("sourceRegion"))
                {
                    $ReplicationObject.Remove("sourceRegion")
                }

                if ($ReplicationObject.ContainsKey("targetRegion"))
                {
                    $ReplicationObject.Remove("targetRegion")
                }

                break
            }
        }

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.DefaultProjectId
        }

        $Splat = @{}

        if (-not [System.String]::IsNullOrEmpty($Session))
        {
            $Splat.Add("Session", $Session)
        }

        if ($ReplicationObject.ContainsKey("subnetId") -and $ReplicationObject["subnetId"] -ieq "default")
        {
            $ReplicationObject["subnetId"] = ""
        }

        # Must specify the region for the target since this won't be set yet in the project
        # If the project has a target region, it's because it has an associated replication
        # configuration, which means we wouldn't be creating a new one
        if ($PSBoundParameters.ContainsKey("TargetRegion"))
        {
            $ReplicationObject.Add("region", $PSBoundParameters["TargetRegion"])
        }

        if ($ReplicationObject.Count -gt 0)
        {
            [System.Collections.Hashtable]$ExistingConfiguration = Get-CEReplicationConfiguration -Id $Id -ProjectId $ProjectId @SessSplat | ConvertTo-Hashtable                    
            [System.Collections.Hashtable]$NewConfig = Merge-HashTables -Source $ExistingConfiguration -Update $ReplicationObject

            $Body = ConvertTo-Json -InputObject $NewConfig -Compress
            Write-Verbose -Message "Sending updated config:`n$Body"

            [System.String]$UrlPath = "/projects/$ProjectId/replicationConfigurations/$Id"

            $ErrorHandling = {
                Param($StatusCode, $Content, $ErrorMessage)
            
                switch ($StatusCode) {
                    200 {    
                        Write-Verbose -Message $Content
                        Write-Output -InputObject (ConvertFrom-Json -InputObject $Content)
                        break
                    }
                    400 {
                        throw "There is a conflict in the replication configuration. This can be due to: subnet ID which does not exist in the region, security groups that are not in the same network as the subnet, etc."
                    }
                    default {
                        # Make sure we don't send the patch request if this failed
                        throw "Failed to update the Replication Configuration $Id with error: $ErrorMessage"
                    }
                }
            }

            $ConfirmMessage = ""

            if ($PSBoundParameters.ContainsKey("TargetRegion"))
            {
                $ConfirmMessage += @"
Warning!
 
Changing your Migration Target is destructive!
 
Saving this change will cause all current machines to be removed from the CloudEndure User Console: you will need to reinstall the CloudEndure Agent on all the machines and Data Replication will restart from zero.
 
"@

            }

            if ($ReplicationObject.ContainsKey("VolumeEncryptionKey"))
            {
                $ConfirmMessage += @"
Warning!
 
Changing the volume encryption key is destructive!
 
Saving this change will cause Data Replication to restart from zero.
 
"@

            }

            if ($ReplicationObject.ContainsKey("CloudCredentials"))
            {
                $ConfirmMessage += @"
Warning!
 
Changing the volume encryption key is destructive!
 
Saving this change will cause Data Replication to restart from zero.
 
"@

            }

            $ConfirmMessage += "Are you sure you want to update the replication configuration?"

            $WhatIfDescription = "Updated configuration to $Body"
            $ConfirmCaption = "Update Replication Configuration"

            if ($Force -or $PSCmdlet.ShouldProcess($WhatIfDescription, $ConfirmMessage, $ConfirmCaption))
            {
                $Result = Invoke-CERequest -Path $UrlPath -Method Patch -Body $Body -Session $Session -ErrorHandling $ErrorHandling

                if ($PassThru)
                {
                    Write-Output -InputObject $Result
                }
            }
        }
        else
        {
            Write-Verbose -Message "No updates provided for replication configuration."
        }
    }

    End {
    }
}

Function Remove-CEReplicationConfiguration {
    <#
        .SYNOPSIS
            Removes a replication configuration. NOT YET SUPPORTED!
 
        .DESCRIPTION
            This cmdlet removes a specified replication configuration. NOT YET SUPPORTED!
 
        .PARAMETER Id
            The id of the replication configuration to remove.
 
        .PARAMETER ProjectId
            The project Id to use to retrieve the details. This defaults to the current project retrieved from the login.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .PARAMETER PassThru
            If specified, the deleted configuration is returned to the pipeline.
 
        .EXAMPLE
            Remove-CEReplicationConfiguration -Id 2ff58f32-cb82-4c41-accc-3001a104c560
 
            Removes the replication configuration with the provided Id.
 
        .INPUTS
            System.Guid
 
        .OUPUTS
            None or System.Management.Automation.PSCustomObject
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/21/2019
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "HIGH")]
    [OutputType([PSCustomObject])]
    Param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$Id,

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty,

        [Parameter()]
        [Switch]$PassThru
    )

    Begin {
        throw "Cmdlet not implemented."
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.DefaultProjectId
        }

        [System.String]$Uri = "/projects/$ProjectId/replicationConfigurations/$($Id.ToString())"

        $ConfirmMessage = "You are about to remove replication configuration $Id."
        $WhatIfDescription = "Removed replication configuration $Id"
        $ConfirmCaption = "Delete CE Replication Configuration"
        
        if ($Force -or $PSCmdlet.ShouldProcess($WhatIfDescription, $ConfirmMessage, $ConfirmCaption))
        {
            try {
                $Result = Invoke-CERequest -Path $UrlPath -Method Delete -Session $Session -ExpectedResponseCode 204

                if ($PassThru)
                {
                    Write-Output -InputObject $Result
                }
            }
            catch [Exception] {
                throw $_.Exception
            }
        }
    }

    End {
    }
}

#endregion

#region User

Function Get-CEUser {
    <#
        .SYNOPSIS
            Gets the current CloudEndure user information.
 
        .DESCRIPTION
            The cmdlet gets the current CloudEndure user information
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
             
        .EXAMPLE
            Get-CEUser
 
            Gets the current user information.
 
        .INPUTS
            None
 
        .OUTPUTS
           PSCustomObject
 
            This is a JSON representation of the returned value:
            {
              "username": "user@example.com",
              "status": "PENDING",
              "account": "string",
              "roles": [
                "USER"
              ],
              "settings": {
                "sendNotifications": {
                  "projectIDs": [
                    "string"
                  ]
                }
              },
              "apiToken": "string",
              "hasPassword": true,
              "termsAccepted": true,
              "id": "string",
              "selfLink": "string"
            }
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/15/2019
    #>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSCustomObject])]
    Param(
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {        
    }

    Process {     
        [System.String]$Path = "/me"

        try {
            $Result = Invoke-CERequest -Path $Path -Method Get -Session $Session -ExpectedResponseCode 200
            Write-Output -InputObject $Result
        }
        catch [Exception] {
            throw "There was an issue retrieving the user info: $($_.Exception.Message)"
        }    
    }

    End {
    }
}

Function Set-CEConsolePassword {
    <#
        .SYNOPSIS
            Updates the password associated with the console logon.
 
        .DESCRIPTION
            The cmdlet updates the CE account password used to logon to the console.
 
        .PARAMETER OldPassword
            The current password for the account.
 
        .PARAMETER NewPassword
            The new password for the account. It must 8 characters or more, 1 upper, 1 lower, 1 numeric, and 1 special character.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .EXAMPLE
            Set-CEConsolePassword -OldPassword MyOldP@$$w0rd -NewPassword @$3cureP@$$w0rd
             
            The cmdlet updates the password.
 
        .INPUTS
            PSObject
 
        .OUTPUTS
            None
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/15/2019
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "HIGH")]
    [OutputType()]
    Param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]$OldPassword,

        [Parameter(Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $_ -ne $OldPassword
        })]
        [ValidatePattern("^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$@$!%*?&])[A-Za-z\d$@$!%*?&]{8,}")]
        [System.String]$NewPassword,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }

    Process {
        [System.String]$Path = "/changePassword"

        [System.Collections.Hashtable]$Body = @{
            "oldPassword" = $OldPassword;
            "newPassword" = $NewPassword
        }

        $ConfirmMessage = "Are you sure you want to update the console password?"
        $WhatIfDescription = "Updated password for $Session."
        $ConfirmCaption = "Update Console Password for $Session"

        if ($Force -or $PSCmdlet.ShouldProcess($WhatIfDescription, $ConfirmMessage, $ConfirmCaption))
        {
            Write-Verbose -Message "Sending updated config:`r`n $(ConvertTo-Json -InputObject $Body)"
            

            $ErrorHandling = {
                Param($StatusCode, $Content, $ErrorMessage)
                
                switch ($StatusCode)
                {
                    204 {
                        Write-Verbose -Message "Password successfully updated."
                        break
                    }
                    400 {
                        throw "Password change did not succeed (e.g. Old password mismatch).`r`n$Content"
                    }
                    default {
                        throw "There was an issue with changing the password: $ErrorMessage"
                    }
                }
            }

            $Result = Invoke-CERequest -Path $Path -Method Post -Body ($Body | ConvertTo-Json) -Session $Session -ErrorHandling $ErrorHandling
        }
    }

    End {
    }
}

Function Set-CEEmailNotifications {
    <#
        .SYNOPSIS
            Sets the email notification status.
 
        .DESCRIPTION
            The cmdlet either disables or enables email notifications.
 
        .PARAMETER Enabled
            Specifies that email notifications will be enabled for the specified projects.
 
        .PARAMETER Disabled
            Specifies that email notifications will be disabled for the specified projects.
 
        .PARAMETER Ids
            The project Ids to enable or disable notifications for. This defaults to the current project retrieved from the login.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .PARAMETER PassThru
            Will pass through the updated user config to the pipeline.
             
        .EXAMPLE
            Set-CEEmailNotifications -Enabled
 
            Enables email notifications on the default for the current user.
 
        .EXAMPLE
            Set-CEEmailNotifications -Disabled -Ids @("c933c984-6dae-431b-a1f4-3063e66c438f")
 
            Disables notifications for the specified project in the current user's settings.
 
        .INPUTS
            None
 
        .OUTPUTS
            None or System.Management.Automation.PSCustomObject
 
            This is a JSON representation of the returned value
 
            {
              "username": "user@example.com",
              "account": "string",
              "agentInstallationToken": "string",
              "settings": {
                "sendNotifications": {
                  "projectIds": [
                    "string"
                  ]
                }
              },
              "id": "string",
              "selfLink": "string"
            }
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/15/2019
    #>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSCustomObject])]
    Param(
        [Parameter(Mandatory = $true, ParameterSetName = "Enabled")]
        [Switch]$Enabled,

        [Parameter(Mandatory = $true, ParameterSetName = "Disabled")]
        [Switch]$Disabled,

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid[]]$Ids = @(),

        [Parameter()]
        [Switch]$PassThru,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {        
    }

    Process {

        $SessionInfo = Get-CESessionOrDefault -Session $Session

        [System.String]$Path = "/users/$($SessionInfo.UserId)"

        if ($Enabled) 
        {
            if ($Ids.Length -eq 0)
            {
                $Ids += $SessionInfo.DefaultProjectId
            }
        }
        else # Disable notifications
        {
            $CurrentSetup = Get-CEUser -Session $Session
            [System.Guid[]]$CurrentProjects = $CurrentSetup.Settings.SendNotifications.ProjectIds
            
            [System.Guid[]]$RemaingProjects = @()

            # Remove the Ids specified by the user by adding the current items that don't match
            # to a temporary array
            foreach ($Id in $CurrentProjects)
            {
                if (-not $Ids.Contains($Id))
                {
                    $RemaingProjects += $Id
                }
            }

            # Iterate again to warn the user if they specified projects that weren't currently enabled
            foreach ($Id in $Ids)
            {
                if (-not $CurrentProjects.Contains($Id))
                {
                    Write-Warning -Message "Could not find a project $Id that was enabled for notifications for the current user."
                }
            }

            $Ids = $RemaingProjects
        }

        [System.String]$Body = ConvertTo-Json -InputObject @{
            "username" = $SessionInfo.User.username; 
            "settings" = @{"sendNotifications" = @{"projectIDs" = $Ids}}
        } -Depth 3

        Write-Verbose -Message "Setting email notifications update:`r`n$Body"
        
        $ErrorHandling = {
            Param(
                $StatusCode,
                $Content,
                $ErrorMessage,
                $Enabled,
                $Username
            )

            switch ($StatusCode)
            {
                200 {
                    if ($Enabled) 
                    {
                        Write-Verbose -Message "Email notifications enabled for $Username."
                    }
                    else 
                    {
                        Write-Verbose -Message "Email notifications disabled for $Username."
                    }

                    Write-Output -InputObject ([PSCustomObject](ConvertFrom-Json -InputObject $Content))

                    break
                }
                401 {
                    throw "Tried patching a user different to the currently logged in one.`r`n$Content"
                }
                404 {
                    throw "Cannot apply the project ids provided.`r`n$Content"
                }
                default {
                    throw "Email notifications could not be set properly, $ErrorMessage"
                }
            }
        }

        try {
            $Result = Invoke-CERequest -Body $Body -Path $Path -Method Patch -Session $Session -ErrorHandling $ErrorHandling -ErrorHandlingArgs @($Enabled, $SessionInfo.User.username)
            Write-Output -InputObject $Result
        }
        catch [Exception] {
            throw "There was an issue setting the notification settings: $($_.Exception.Message)"
        }
    }

    End {
    }
}

#endregion

#region Accounts

Function Get-CEAccount {
    <#
        .SYNOPSIS
            CloudEndure service account information.
 
        .DESCRIPTION
            CloudEndure service account information.
 
        .PARAMETER AccountId
            The account Id to retrieve information about. This defaults to the user account retrieved during login.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .EXAMPLE
            Get-CEAccount
 
            Gets the account data.
 
        .INPUTS
            None
 
        .OUTPUTS
            System.Management.Automation.PSCustomObject
 
            This is a JSON representation of the returned object.
            {
                "inviteTokenExpiryMinutes": 0,
                "allowArchivingDefaultValue": true,
                "perAccountUserPool": true,
                "isGcpSelfService": true,
                "isDrTrial": true,
                "isArmSelfService": true,
                "isAwsSelfService": true,
                "samlSettings": {
                    "identityProviderCertificate": "string",
                    "identityProviderUrl": "string",
                    "identityProviderID": "string"
                },
                "isRightSizingEnabled": true,
                "defaultLicenseType": "MIGRATION",
                "maxProjectsAllowed": 0,
                "ceAdminProperties": {
                    "state": "ACTIVE",
                    "version": "string",
                    "accountOwnerUsername": "string",
                    "apisPerMinute": 0,
                    "comments": "string",
                    "history": "string"
                },
                "ownerId": "string",
                "isMedOne": true,
                "id": "string"
            }
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/15/2019
    #>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSCustomObject])]
    Param(
        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$AccountId = [System.Guid]::Empty,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }

    Process {

        if ($AccountId -eq [System.Guid]::Empty)
        {
            $SessionInfo = Get-CESessionOrDefault -Session $Session
            $AccountId = [System.Guid]::Parse($SessionInfo["User"].account)
        }

        [System.String]$Path = "/accounts/$AccountId"

        try {
            $Result = Invoke-CERequest -Path $Path -Method Get -Session $Session -ExpectedResponseCode 200
            Write-Output -InputObject $Result
        }
        catch [Exception] {
            throw "There was an issue retrieving the CE account: $($_.Exception.Message)"
        }
    }

    End {
    }
}

Function Get-CEAccountExtendedInfo {
     <#
        .SYNOPSIS
            Returns the extended current account information.
 
        .DESCRIPTION
            This cmdlet returns the extended current account information.
 
            -Account (Features & Id)
            -Clouds (Configured cloud environments)
            -Generic Region
            -DateTime (Current time)
            -User
            -License
            -Projects
            -ReplicationConfiguration
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .EXAMPLE
            Get-CEAccountExtendedInfo
 
            Gets the extended account information
 
        .INPUTS
            None
 
        .OUTPUTS
            PSCustomObject
 
            This is a JSON representation of the return object:
 
            {
              "account": {
                "inviteTokenExpiryMinutes": 0,
                "allowArchivingDefaultValue": true,
                "perAccountUserPool": true,
                "isGcpSelfService": true,
                "isDrTrial": true,
                "isArmSelfService": true,
                "isAwsSelfService": true,
                "samlSettings": {
                  "identityProviderCertificate": "string",
                  "identityProviderUrl": "string",
                  "identityProviderID": "string"
                },
                "isRightSizingEnabled": true,
                "defaultLicenseType": "MIGRATION",
                "maxProjectsAllowed": 0,
                "ceAdminProperties": {
                  "state": "ACTIVE",
                  "version": "string",
                  "accountOwnerUsername": "string",
                  "apisPerMinute": 0,
                  "comments": "string",
                  "history": "string"
                },
                "ownerId": "string",
                "isMedOne": true,
                "id": "string"
              },
              "clouds": {
                "items": [
                  {
                    "id": "string",
                    "roles": [
                      "SOURCE"
                    ],
                    "name": "AWS"
                  }
                ]
              },
              "genericRegion": {
                "subnets": [
                  {
                    "subnetId": "string",
                    "networkId": "string",
                    "name": "string"
                  }
                ],
                "placementGroups": [
                  "string"
                ],
                "scsiAdapterTypes": [
                  "string"
                ],
                "instanceTypes": [
                  "string"
                ],
                "zones": [
                  "string"
                ],
                "volumeEncryptionKeys": [
                  "string"
                ],
                "cloud": "string",
                "securityGroups": [
                  {
                    "networkId": "string",
                    "securityGroupId": "string",
                    "name": "string"
                  }
                ],
                "logicalLocations": [
                  {
                    "locationId": "string",
                    "name": "string"
                  }
                ],
                "staticIps": [
                  "string"
                ],
                "maxCpusPerMachine": 0,
                "networkInterfaces": [
                  {
                    "subnetId": "string",
                    "name": "string",
                    "privateIp": "string"
                  }
                ],
                "computeLocations": [
                  {
                    "isEncryptionSupported": true,
                    "locationId": "string",
                    "name": "string"
                  }
                ],
                "name": "string",
                "storageLocations": [
                  {
                    "locationId": "string",
                    "name": "string"
                  }
                ],
                "iamRoles": [
                  "string"
                ],
                "id": "string",
                "maxCoresPerMachineCpu": 0,
                "dedicatedHosts": [
                  "string"
                ],
                "networkAdapterTypes": [
                  "string"
                ],
                "maxMbRamPerMachine": 0
              },
              "dateTime": {
                "dateTime": "2019-10-15T16:25:26Z"
              },
              "user": {
                "username": "user@example.com",
                "status": "PENDING",
                "account": "string",
                "roles": [
                  "USER"
                ],
                "settings": {
                  "sendNotifications": {
                    "projectIDs": [
                      "string"
                    ]
                  }
                },
                "apiToken": "string",
                "hasPassword": true,
                "termsAccepted": true,
                "id": "string",
                "selfLink": "string"
              },
              "projects": {
                "items": [
                  {
                    "targetCloudId": "string",
                    "agentInstallationToken": "string",
                    "name": "string",
                    "usersIDs": [
                      "string"
                    ],
                    "type": "MIGRATION",
                    "replicationReversed": true,
                    "sourceCloudCredentialsId": "string",
                    "cloudCredentialsIDs": [
                      "string"
                    ],
                    "sourceRegion": "string",
                    "licensesIDs": [
                      "string"
                    ],
                    "ceAdminProperties": {
                      "comments": "string",
                      "history": "string"
                    },
                    "replicationConfiguration": "string",
                    "sourceCloudId": "string",
                    "id": "string",
                    "features": {
                      "awsExtendedHddTypes": true,
                      "allowRecoveryPlans": true,
                      "allowArchiving": true,
                      "isDemo": true,
                      "drTier2": true,
                      "allowByolOnDedicatedInstance": true,
                      "pit": true
                    }
                  }
                ]
              },
              "isNewlyRegistered": true,
              "replicationConfigurations": {
                "items": [
                  {
                    "volumeEncryptionKey": "string",
                    "replicationTags": [
                      {
                        "key": "string",
                        "value": "string"
                      }
                    ],
                    "disablePublicIp": true,
                    "subnetHostProject": "string",
                    "replicationServerType": "string",
                    "useLowCostDisks": true,
                    "computeLocationId": "string",
                    "cloudCredentials": "string",
                    "subnetId": "string",
                    "logicalLocationId": "string",
                    "bandwidthThrottling": 0,
                    "useDedicatedServer": true,
                    "zone": "string",
                    "replicatorSecurityGroupIDs": [
                      "string"
                    ],
                    "usePrivateIp": true,
                    "region": "string",
                    "id": "string",
                    "proxyUrl": "string",
                    "volumeEncryptionAllowed": true,
                    "objectStorageLocation": "string",
                    "archivingEnabled": true,
                    "storageLocationId": "string"
                  }
                ]
              }
            }
 
         .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/15/2019
    #>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSCustomObject])]
    Param(
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }

    Process {
        [System.String]$Path = "/extendedAccountInfo"

        try {
            $Result = Invoke-CERequest -Path $Path -Method Get -Session $Session -ExpectedResponseCode 200
            Write-Output -InputObject $Result
        }
        catch [Exception] {
            throw "There was an issue retrieving the account summary: $($_.Exception.Message)"
        }    
    }

    End {
    }
}

#endregion

#region Licenses

Function Get-CELicense {
    <#
        .SYNOPSIS
           Gets the current state of license information.
 
        .DESCRIPTION
            The cmdlet lists the license information about the specified account.
 
        .PARAMETER Id
            The Id of the license to retrieve.
 
        .PARAMETER Offset
            With which item to start (0 based).
 
        .PARAMETER Limit
            A number specifying how many entries to return between 0 and 1500 (defaults to 1500).
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
             
        .EXAMPLE
            Get-CELicense
 
            Retrieves the licenses in the account using the default session context.
 
        .INPUTS
            None
 
        .OUTPUTS
            System.Management.Automation.PSCustomObject or System.Management.Automation.PSCustomObject[]
 
            This is a JSON representation of the returned array:
            [
                {
                  "count": 0,
                  "durationFromStartOfUse": "string",
                  "used": 0,
                  "expirationDateTime": "2017-09-06T01:39:46Z",
                  "type": "MIGRATION",
                  "id": "string"
                }
            ]
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/15/2019
    #>

    [CmdletBinding(DefaultParameterSetName = "List")]
    [OutputType([System.Management.Automation.PSCustomObject[]])]
    Param(
        [Parameter(ValueFromPipeline = $true, ParameterSetName = "Get")]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$Id = [System.Guid]::Empty,

        [Parameter(ParameterSetName = "List")]
        [ValidateRange(0, [System.UInt32]::MaxValue)]
        [System.UInt32]$Offset = 0,

        [Parameter(ParameterSetName = "List")]
        [ValidateRange(0, 1500)]
        [System.UInt32]$Limit = 1500,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {        
    }

    Process {
        [System.String]$Path = "/licenses"

        switch ($PSCmdlet.ParameterSetName)
        {
            "Get" {
                if ($Id -ne [System.Guid]::Empty)
                {
                    $Path += "/$($Id.ToString())"
                }
                break
            }
            "List" {
                if ($Offset -gt 0 -or $Limit -lt 1500)
                {
                    $QueryString = [System.String]::Empty

                    if ($Offset -gt 0)
                    {
                        $QueryString += "&offset=$Offset"
                    }

                    if ($Limit -lt 1500)
                    {
                        $QueryString += "&limit=$Limit"
                    }

                    # Remove the first character which is an unecessary ampersand
                    $Path += "?$($QueryString.Substring(1))"
                }
                break
            }
            default {
                throw "Encountered an unknown parameter set $($PSCmdlet.ParameterSetName)."
            }
        }

        try {

            $Result = Invoke-CERequest -Path $Path -Method Get -Session $Session -ExpectedResponseCode 200
            
            [PSCustomObject[]]$Return = @()

            if ($Id -ne [System.Guid]::Empty)
            {
                $Return += $Result
            }
            else 
            {
                $Return += $Result.Items
            }

            # Force PowerShell to stop unboxing 1 item arrays
            # Cannot be used with Write-Output
            ,$Return
        }
        catch [Exception] {
            throw "There was an issue retrieving the license information: $($_.Exception.Message)"
        }    
    }

    End {
    }
}

#endregion

#region Project

Function New-CEProject {
    <#
        .SYNOPSIS
            Creates a new CloudEndure project.
 
        .DESCRIPTION
            Creates a new CloudEndure project.
 
        .PARAMETER Config
            The config to use to create the project.
 
            {
              "targetCloudId": "string",
              "name": "string",
              "usersIDs": [
                "string"
              ],
              "cloudCredentialsIDs": [
                "string"
              ],
              "sourceRegion": "string",
              "licensesIDs": [
                "string"
              ],
              "replicationConfiguration": "string"
            }
 
        .PARAMETER TargetCloudId
            The id of the target cloud environment.
 
        .PARAMETER Name
            The name of the project.
 
        .PARAMETER UserIds
            Empty array.
 
        .PARAMETER CloudCredentialsIDs
            An array of 1 cloud credentials to use. This defaults to the current session.
 
        .PARAMETER SourceRegion
            The id of the source region to use.
 
        .PARAMETER LicenseIds
            The IDs of the licenses associated with this project (array of one).
         
        .PARAMETER ReplicationConfiguration
            The Id of the replication configuration for the project to use.
 
        .PARAMETER PassThru
            If specified, the updated configuration is returned to the pipeline.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .EXAMPLE
            New-CEProject -SourceRegion "Generic" -TargetCloudId "6849e59c-29f5-4e10-a459-9d8584c7524b" -Name "MyAWSMigration" -ReplicationConfiguration 0cd58880-2ba0-469c-95f6-ed851f034145
 
            Creates a new project for migrating from a Generic source to AWS.
 
        .INPUTS
            None or System.Collections.Hashtable
 
        .OUTPUTS
            None or System.Management.Automation.PSCustomObject
 
            The JSON representation of the returned object:
            {
              "targetCloudId": "string",
              "agentInstallationToken": "string",
              "name": "string",
              "usersIDs": [
                "string"
              ],
              "type": "MIGRATION",
              "replicationReversed": true,
              "sourceCloudCredentialsId": "string",
              "cloudCredentialsIDs": [
                "string"
              ],
              "sourceRegion": "string",
              "licensesIDs": [
                "string"
              ],
              "ceAdminProperties": {
                "comments": "string",
                "history": "string"
              },
              "replicationConfiguration": "string",
              "sourceCloudId": "string",
              "id": "string",
              "features": {
                "awsExtendedHddTypes": true,
                "allowRecoveryPlans": true,
                "allowArchiving": true,
                "isDemo": true,
                "drTier2": true,
                "allowByolOnDedicatedInstance": true,
                "pit": true
              }
            }
         
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/22/2019
    #>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSCustomObject])]
    Param(
        
        [Parameter(ParameterSetName = "Config", Position = 0, ValueFromPipeline = $true, Mandatory = $true)]
        [ValidateNotNull()]
        [System.Collections.Hashtable]$Config = @{},

        [Parameter(ParameterSetName = "Path", Mandatory =$true)]
        [ValidateScript({
            Test-Path -Path $_
        })]
        [System.String]$Path = [System.String]::Empty,

        [Parameter()]
        [ValidateSet("MIGRATION")]
        [System.String]$Type = "MIGRATION",

        [Parameter(ParameterSetName = "Individual")]
        [System.String]$Name = [System.String]::Empty,

        [Parameter(ParameterSetName = "Individual")]
        [ValidateLength(1, 1)]
        [System.Guid[]]$CloudCredentialsIDs = @(),

        [Parameter(ParameterSetName = "Individual", DontShow = $true)]
        [ValidateLength(0, 1)]
        [System.Guid[]]$UsersIds = @(),

        [Parameter(ParameterSetName = "Individual")]
        [ValidateLength(1, 1)]
        [System.Guid[]]$LicenseIds = @(),

        [Parameter()]
        [Switch]$PassThru,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    DynamicParam {

        # Create the dictionary
        $RuntimeParameterDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary

        [System.Collections.Hashtable]$TargetCloudData = @{}

        if (($Config -eq $null -or $Config -eq @{}) -and [System.String]::IsNullOrEmpty($Path))
        {
            $DynSplat = @{}

            if ($ProjectId -ne $null -and $ProjectId -ne [System.Guid]::Empty)
            {
                $DynSplat.Add("ProjectId", $ProjectId)
            }

            if (-not [System.String]::IsNullOrEmpty($Session))
            {t
                $DynSplat.Add("Session", $Session)
            }

            $TargetCloudData = @{}

            $AccountInfo = Get-CEAccountExtendedInfo @DynSplat

            $AccountInfo | Select-Object -ExpandProperty Clouds | Select-Object -ExpandProperty Items | ForEach-Object {
                $TargetCloudData.Add($_.Name, $_.Id)
            }

            $SourceRegionData = @{}

            Get-CECloudRegion @DynSplat | ForEach-Object {
                $SourceRegionData.Add($_.Name, $_.Id)                
            }

            $SourceRegionData.Add("GENERIC", $AccountInfo.GenericRegion.Id)

            New-DynamicParameter -Name "TargetCloudName" -Type ([System.String]) -ValidateSet $TargetCloudData.Keys -ParameterSets @("Individual") -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
            New-DynamicParameter -Name "TargetCloudId" -Type ([System.Guid]) -ValidateSet $TargetCloudData.Values -ParameterSets @("Individual") -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
            
            New-DynamicParameter -Name "SourceRegionName" -Type ([System.String]) -ValidateSet $SourceRegionData.Keys -ParameterSets @("Individual") -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
            New-DynamicParameter -Name "SourceRegion" -Type ([System.String]) -ValidateSet $SourceRegionData.Values -ParameterSets @("Individual") -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
            #New-DynamicParameter -Name "ReplicationConfiguration" -Type ([System.Guid]) -Mandatory -ValidateSet(Get-CEReplicationConfiguration @DynSplat | Select-Object -ExpandProperty Id) -ParameterSets @("Individual") -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
        }

        return $RuntimeParameterDictionary
    }

    Begin {

    }

    Process {

        $Body = ""
        $ProjectObject = [PSCustomObject]@{}

        switch ($PSCmdlet.ParameterSetName)
        {
            "Path" {
                $ProjectObject = Get-Content -Path $Path -Raw | ConvertFrom-Json
                break
            }
            "Config" {
                $ProjectObject = [PSCustomObject]$Config
                break
            }
            default {
                if ($PSBoundParameters.ContainsKey("TargetCloudName"))
                {
                    $PSBoundParameters["TargetCloudId"] = $TargetCloudData[$PSBoundParameters["TargetCloudName"]]
                }

                if ($PSBoundParameters.ContainsKey("SourceRegionName"))
                {
                    $PSBoundParameters["SourceRegion"] = $SourceRegionData[$PSBoundParameters["SourceRegionName"]]
                }

                # This is going to take all of the parameters supplied, put them into a hash table, and then create a json
                # document that is the blueprint
                $Config = Convert-ParametersToHashtable -Parameters (Get-Command -Name $PSCmdlet.MyInvocation.InvocationName).Parameters `
                    -ParameterSetName $PSCmdlet.ParameterSetName `
                    -RuntimeParameterDictionary $RuntimeParameterDictionary `
                    -BoundParameters $PSBoundParameters 

                $ProjectObject = [PSCustomObject]$Config

                $ProjectObject = $ProjectObject | Select-Object -Property * -ExcludeProperty TargetCloudName
            }
        }

        # Requiring Type (or that it is a parameter at all) is not documented in the API guide as of 10/22/2019
        if (-not ($ProjectObject | Get-Member -Name "Type" -MemberType Properties))
        {
            $ProjectObject | Add-Member -Name "type" -MemberType NoteProperty -Value $Type
        }

        if (-not ($ProjectObject | Get-Member -Name "TargetCloudId" -MemberType Properties))
        {
            throw "You must specify either the TargetCloudName or TargetCloudId property."
        }
        
        [System.String]$UrlPath = "/projects"

        $Body = ConvertTo-Json -InputObject $ProjectObject

        Write-Verbose -Message "Submitting new project:`n$Body"

        $ErrorHandling = {
            Param(
                $StatusCode,
                $Content,
                $ErrorMessage
            )

            switch ($StatusCode)
            {
                201 {
                    Write-Output -InputObject ([PSCustomObject](ConvertFrom-Json -InputObject $Content))
                    break
                }
                400 {
                    throw "Max projects per Account reached.`n$ErrorMessage"
                }
                409 {
                    throw "Cannot be completed due to conflict.`n$ErrorMessage"
                }
                default {
                    throw "There was an issue creating the project: $ErrorMessage"
                }
            }
        }

        $Result = Invoke-CERequest -Path $UrlPath -Method Post -Body $Body  -ErrorHandling $ErrorHandling

        if ($PassThru)
        {
            Write-Output -InputObject $Result
        }
    }

    End {
    }
}

Function Get-CEProject {
    <#
        .SYNOPSIS
            Gets basic information about the CE project.
 
        .DESCRIPTION
            The cmdlet retrieves basic information about the CE project in the CE account.
 
        .PARAMETER Id
            The Id of the project to retrieve.
 
        .PARAMETER Current
            Specified that information about the current project retrieved from the loging should be returned.
 
        .PARAMETER Offset
            With which item to start (0 based).
 
        .PARAMETER Limit
            A number specifying how many entries to return between 0 and 1500 (defaults to 1500).
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
             
        .EXAMPLE
            Get-CEProject
 
            Retrieves all projects up to 1500.
 
        .EXAMPLE
            Get-CEProject -Current
             
            Retrieves data about the current project.
 
        .INPUTS
            None or System.Guid
 
        .OUTPUTS
           System.Management.Automation.PSCustomObject or System.Management.Automation.PSCustomObject[]
 
            This is a JSON representation of the returned array:
            [
                {
                  "targetCloudId": "string",
                  "agentInstallationToken": "string",
                  "name": "string",
                  "usersIDs": [
                    "string"
                  ],
                  "type": "MIGRATION",
                  "replicationReversed": true,
                  "sourceCloudCredentialsId": "string",
                  "cloudCredentialsIDs": [
                    "string"
                  ],
                  "sourceRegion": "string",
                  "licensesIDs": [
                    "string"
                  ],
                  "ceAdminProperties": {
                    "comments": "string",
                    "history": "string"
                  },
                  "replicationConfiguration": "string",
                  "sourceCloudId": "string",
                  "id": "string",
                  "features": {
                    "awsExtendedHddTypes": true,
                    "allowRecoveryPlans": true,
                    "allowArchiving": true,
                    "isDemo": true,
                    "drTier2": true,
                    "allowByolOnDedicatedInstance": true,
                    "pit": true
                  }
                }
            ]
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/16/2019
    #>

    [CmdletBinding(DefaultParameterSetName = "List")]
    [OutputType([PSCustomObject], [PSCustomObject[]])]
    Param(
        [Parameter(ParameterSetName = "Current")]
        [Switch]$Current,

        [Parameter(ValueFromPipeline = $true, Mandatory = "true", Position = 0, ParameterSetName = "Get")]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$Id = [System.Guid]::Empty,

        [Parameter(ParameterSetName = "List")]
        [ValidateRange(0, [System.UInt32]::MaxValue)]
        [System.UInt32]$Offset = 0,

        [Parameter(ParameterSetName = "List")]
        [ValidateRange(0, 1500)]
        [System.UInt32]$Limit = 1500,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {        
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        [System.String]$Path = "/projects"

        switch ($PSCmdlet.ParameterSetName)
        {
            "Get" {
                $Path += "/$($Id.ToString())"

                break
            }
            "List" {
                if ($Offset -gt 0 -or $Limit -lt 1500)
                {
                    $QueryString = [System.String]::Empty

                    if ($Offset -gt 0)
                    {
                        $QueryString += "&offset=$Offset"
                    }

                    if ($Limit -lt 1500)
                    {
                        $QueryString += "&limit=$Limit"
                    }

                    # Remove the first character which is an unecessary ampersand
                    $Path += "?$($QueryString.Substring(1))"
                }
                break
            }
            "Current" {
                $Path += "/$($SessionInfo["DefaultProjectId"])"
                break
            }
            default {
                Write-Warning -Message "Encountered an unknown parameter set $($PSCmdlet.ParameterSetName)."
                break
            }
        }
        
        try {
            $Result = Invoke-CERequest -Path $Path -Method Get -Session $Session -ExpectedResponseCode 200
        }
        catch [Exception] {
            throw "There was an issue retrieving the project information: $($_.Exception.Message)"
        }

        if ($PSCmdlet.ParameterSetName -ieq "List")
        {
            Write-Output -InputObject $Result.Items            
        }
        else 
        {
            Write-Output -InputObject $Result
        }
    }

    End {
    }
}

Function Set-CEProject {
    <#
        .SYNOPSIS
            Configure project's source location, replication settings, etc.
 
        .DESCRIPTION
            Configure project's source location, replication settings, etc.
 
        .PARAMETER ProjectId
            The Id of the project to set. If this is not specified, the current project is used.
 
        .PARAMETER Config
            The config to update the project's settings.
 
            {
              "targetCloudId": "string",
              "name": "string",
              "cloudCredentialsIDs": [
                "string"
              ],
              "sourceRegion": "string",
              "replicationConfiguration": "string"
            }
 
        .PARAMETER Path
            The path to the configuration file for the project update.
 
        .PARAMETER Target
            The Name of the target cloud environment to use.
 
        .PARAMETER Name
            The name of the project.
 
        .PARAMETER CloudCredentialsIDs
            An array of 1 cloud credentials to use. This defaults to the current session.
         
        .PARAMETER ReplicationConfiguration
            The Id of the replication configuration for the project to use.
 
        .PARAMETER Source
            The Name of the source cloud region/environment to use. Use either this or SourceId, but not both.
 
        .PARAMETER SourceId
            The Name of the source cloud region/environment to use. Use either this or Source, but not both.
 
        .PARAMETER PassThru
            If specified, the updated configuration is returned to the pipeline.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .EXAMPLE
            Set-CEProject -Source "Generic"
 
            Sets the current project to use the source replication environment as "Generic".
 
        .EXAMPLE
            Set-CEProject -Source "Generic" -Target "AWS" -Name "MyAWSMigration"
 
            Updates the current project to use a generic source (i.e on-premises), a destination of AWS, and names the project MyAWSMigration.
 
        .INPUTS
            None or System.Collections.Hashtable
 
        .OUTPUTS
            None or System.Management.Automation.PSCustomObject
 
            The JSON representation of the returned object:
            {
              "source": "string",
              "replicationConfiguration": "string",
              "id": "string",
              "name": "string",
              "type": "MIGRATION"
            }
         
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 11/19/2019
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "HIGH")]
    [OutputType([System.Management.Automation.PSCustomObject])]
    Param(
        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter(ParameterSetName = "Config", Position = 0, ValueFromPipeline = $true, Mandatory = $true)]
        [System.Collections.Hashtable]$Config = @{},

        [Parameter(ParameterSetName = "Path")]
        [ValidateScript({
            Test-Path -Path $_
        })]
        [System.String]$Path,

        [Parameter(ParameterSetName = "Individual")]
        [System.String]$Name,

        [Parameter(ParameterSetName = "Individual")]
        [ValidateScript({
            $_.Length -eq 1
        })]
        [System.Guid[]]$CloudCredentialsIDs = @(),

        [Parameter()]
        [Switch]$PassThru,

        [Parameter()]
        [Switch]$Force,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    DynamicParam {

        # Create the dictionary
        $RuntimeParameterDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary

        if ($Config -eq $null -or $Config -eq @{})
        {
            $SessionInfo = Get-CESessionOrDefault -Session $Session

            $DynSplat = @{
                "Session" = $SessionInfo.User.Username
            }

            if ($ProjectId -eq $null -or $ProjectId -eq [System.Guid]::Empty)
            {
                $ProjectId = $SessionInfo.DefaultProjectId
            }

            $DynSplat.Add("ProjectId", $ProjectId)

            $Region = Get-CECloudRegion @DynSplat

            New-DynamicParameter -Name "Source" -Type ([System.String]) -ValidateSet (($Region | Select-Object -ExpandProperty Name) + "Generic") -ParameterSets @("Individual") -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
            New-DynamicParameter -Name "SourceId" -Type ([System.String]) -ValidateSet (($Region | Select-Object -ExpandProperty Id) + $SessionInfo["GenericRegionId"]) -ParameterSets @("Individual") -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null            
            
            New-DynamicParameter -Name "Target" -Type ([System.String]) -ValidateSet ($script:CloudIds.Keys) -ParameterSets @("Individual") -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null

            $ReplConfigs = Get-CEReplicationConfiguration @DynSplat | Select-Object -ExpandProperty Id

            if ($ReplConfigs -ne $null -and $ReplConfigs.Length -gt 0)
            {
                New-DynamicParameter -Name "ReplicationConfiguration" -Type ([System.Guid]) -Mandatory -ValidateSet $ReplConfigs -ParameterSets @("Individual") -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
            }
        }

        return $RuntimeParameterDictionary
    }

    Begin {
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.DefaultProjectId
        }

        [System.Collections.Hashtable]$SessSplat = @{
            "Session" = $SessionInfo.User.Username
        }

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.DefaultProjectId
        }

        $SessSplat.Add("ProjectId", $ProjectId)

        [PSCustomObject[]]$CERegions = Get-CECloudRegion @SessSplat

        $ConfigObject = [PSCustomObject]@{}
        switch ($PSCmdlet.ParameterSetName)
        {
            "Path" {
                $ConfigObject = Get-Content -Path $Path -Raw | ConvertFrom-Json
                break
            }
            "Config" {
                $ConfigObject = [PSCustomObject]$Config
                break
            }
            default {
                # This is going to take all of the parameters supplied, put them into a hash table, and then create a json
                # document that is the blueprint
                $Config = Convert-ParametersToHashtable -Parameters (Get-Command -Name $PSCmdlet.MyInvocation.InvocationName).Parameters `
                        -ParameterSetName $PSCmdlet.ParameterSetName `
                        -RuntimeParameterDictionary $RuntimeParameterDictionary `
                        -BoundParameters $PSBoundParameters 

                if ($Config.ContainsKey("source")) {
                    $SourceId = (($CERegions | Select-Object Name,Id) + [PSCustomObject]@{"Name" = "Generic"; "Id" = $SessionInfo["GenericRegionId"]}) | Where-Object {$_.Name -ieq $Config["source"]} | Select-Object -First 1 -ExpandProperty Id
                    $Config.Add("sourceRegion", $SourceId)
                    $Config.Remove("source")
                }
                
                if ($Config.ContainsKey("sourceId"))
                {
                    $Config.Add("sourceRegion", $Config["sourceId"])
                    $Config.Remove("sourceId")
                }

                if ($Config.ContainsKey("target")) {
                    $Config.Add("targetCloudId", $script:CloudIds[$Config["target"]])
                    $Config.Remove("target")
                }

                $ConfigObject = [PSCustomObject]$Config
            }
        }

        if ($ConfigObject -ne $null)
        {        
            # We need the project to see the original source
            [System.Collections.Hashtable]$CurrentProject = Get-CEProject -Id $SessSplat.ProjectId -Session $SessSplat.Session | ConvertTo-Hashtable

            # Build the confirmation messages with warnings about updates to source and destination
            $ConfirmMessage = "The action you are about to perform is destructive!"

            if (-not [System.String]::IsNullOrEmpty($ConfigObject.Source))
            {        
                $OriginalSrc = $CurrentProject["source"]
                $OriginalSource = ($CERegions + [PSCustomObject]@{"Name" = "Generic"; "Id" = $script:CloudIds["Generic"]} ) | Where-Object {$_.Id -ieq $OriginalSrc } | Select-Object -First 1 -ExpandProperty Name
                
                $ConfirmMessage += "`r`n`r`nChanging your Live Migration Source from $OriginalSource to $($PSBoundParameters["Source"]) will cause all current instances to be disconnected from CloudEndure: you will need to reinstall the CloudEndure Agent on all the instances and data replication will restart from zero."
            }

            if (-not [System.String]::IsNullOrEmpty($ConfigObject.ReplicationConfiguration))
            {
                $ConfirmMessage += "`r`n`r`nChanging your Live Migration Target replication configuration will cause all current instances to be disconnected from CloudEndure: you will need to reinstall the CloudEndure Agent on all the instances and data replication will restart from zero."
            }

            $WhatIfDescription = "Updated project configuration to $(ConvertTo-Json -InputObject $Config)"
            $ConfirmCaption = "Update Project Configuration"

            if ($Force -or $PSCmdlet.ShouldProcess($WhatIfDescription, $ConfirmMessage, $ConfirmCaption))
            {                
                [System.String]$UrlPath = "/projects/$ProjectId"

                $Body = ConvertTo-Json -InputObject $ConfigObject
                Write-Verbose -Message "Sending updated config`r`n$Body"
                
                try {
                    $Result = Invoke-CERequest -Path $UrlPath -Method Patch -Body $Body -Session $Session -ExpectedResponseCode 200

                    if ($PassThru)
                    {
                        Write-Output -InputObject $Result
                    }
                }
                catch [Exception] {
                    throw "There was an issue updating the project: $($_.Exception.Message)"
                }
            }
        }
        else
        {
            Write-Warning -Message "No updated configuration properties specified."
        }
    }

    End {
    }
}

Function Set-CEProjectSourceRegion {
    <#
        .SYNOPSIS
            Configure project's source location.
 
        .DESCRIPTION
            Configure project's source location.
 
        .PARAMETER ProjectId
            The Id of the project to set. If this is not specified, the current project is used.
 
        .PARAMETER Source
            The Name of the source cloud region/environment to use.
 
        .PARAMETER SourceId
            The Id of the source cloud region/environment to use.
 
        .PARAMETER PassThru
            If specified, the updated configuration is returned to the pipeline.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .EXAMPLE
            Set-CEProject -Source "Generic"
 
            Sets the current project to use the source replication environment as "Generic".
 
        .INPUTS
            System.String
 
        .OUTPUTS
            None or System.Management.Automation.PSCustomObject
 
            The JSON representation of the returned object:
            {
              "source": "string",
              "replicationConfiguration": "string",
              "id": "string",
              "name": "string",
              "type": "MIGRATION"
            }
         
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 11/19/2019
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "HIGH", DefaultParameterSetName = "Name")]
    [OutputType([System.Management.Automation.PSCustomObject])]
    Param(
        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [Switch]$PassThru,

        [Parameter()]
        [Switch]$Force,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    DynamicParam {

        # Create the dictionary
        $RuntimeParameterDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary

        if ($Config -eq $null -or $Config -eq @{})
        {
            $SessionInfo = Get-CESessionOrDefault -Session $Session

            $DynSplat = @{
                "Session" = $SessionInfo.User.Username
            }

            if ($ProjectId -eq $null -or $ProjectId -eq [System.Guid]::Empty)
            {
                $ProjectId = $SessionInfo.DefaultProjectId
            }

            $DynSplat.Add("ProjectId", $ProjectId)

            $Region = Get-CECloudRegion @DynSplat

            New-DynamicParameter -Name "Source" -Type ([System.String]) -ValidateSet (($Region | Select-Object -ExpandProperty Name) + "Generic") -ParameterSets @("Name") -Mandatory -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null
            New-DynamicParameter -Name "SourceId" -Type ([System.String]) -ValidateSet (($Region | Select-Object -ExpandProperty Id) + $SessionInfo["GenericRegionId"]) -ParameterSets @("Id") -Mandatory -RuntimeParameterDictionary $RuntimeParameterDictionary | Out-Null                        
        }

        return $RuntimeParameterDictionary
    }

    Begin {
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.DefaultProjectId
        }

        [System.Collections.Hashtable]$SessSplat = @{
            "Session" = $SessionInfo.User.Username
        }

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.DefaultProjectId
        }

        $SessSplat.Add("ProjectId", $ProjectId)

        [PSCustomObject[]]$CERegions = Get-CECloudRegion @SessSplat

        $ConfigObject = [PSCustomObject]@{}
        switch ($PSCmdlet.ParameterSetName)
        {
            "Path" {
                $ConfigObject = Get-Content -Path $Path -Raw | ConvertFrom-Json
                break
            }
            "Config" {
                $ConfigObject = [PSCustomObject]$Config
                break
            }
            default {
                # This is going to take all of the parameters supplied, put them into a hash table, and then create a json
                # document that is the blueprint
                $Config = Convert-ParametersToHashtable -Parameters (Get-Command -Name $PSCmdlet.MyInvocation.InvocationName).Parameters `
                        -ParameterSetName $PSCmdlet.ParameterSetName `
                        -RuntimeParameterDictionary $RuntimeParameterDictionary `
                        -BoundParameters $PSBoundParameters 

                if ($Config.ContainsKey("source")) {
                    $SourceId = (($CERegions | Select-Object Name,Id) + [PSCustomObject]@{"Name" = "Generic"; "Id" = $SessionInfo["GenericRegionId"]}) | Where-Object {$_.Name -ieq $Config["source"]} | Select-Object -First 1 -ExpandProperty Id
                    $Config.Add("sourceRegion", $SourceId)
                    $Config.Remove("source")
                }
                
                if ($Config.ContainsKey("sourceId"))
                {
                    $Config.Add("sourceRegion", $Config["sourceId"])
                    $Config.Remove("sourceId")
                }

                if ($Config.ContainsKey("target")) {
                    $Config.Add("targetCloudId", $script:CloudIds[$Config["target"]])
                    $Config.Remove("target")
                }

                $ConfigObject = [PSCustomObject]$Config
            }
        }

        if ($ConfigObject -ne $null)
        {        
            # We need the project to see the original source
            [System.Collections.Hashtable]$CurrentProject = Get-CEProject -Id $SessSplat.ProjectId -Session $SessSplat.Session | ConvertTo-Hashtable

            # Build the confirmation messages with warnings about updates to source and destination
            $ConfirmMessage = "The action you are about to perform is destructive!"

            if (-not [System.String]::IsNullOrEmpty($ConfigObject.Source))
            {        
                $OriginalSrc = $CurrentProject["source"]
                $OriginalSource = ($CERegions + [PSCustomObject]@{"Name" = "Generic"; "Id" = $script:CloudIds["Generic"]} ) | Where-Object {$_.Id -ieq $OriginalSrc } | Select-Object -First 1 -ExpandProperty Name
                
                $ConfirmMessage += "`r`n`r`nChanging your Live Migration Source from $OriginalSource to $($PSBoundParameters["Source"]) will cause all current instances to be disconnected from CloudEndure: you will need to reinstall the CloudEndure Agent on all the instances and data replication will restart from zero."
            }

            if (-not [System.String]::IsNullOrEmpty($ConfigObject.ReplicationConfiguration))
            {
                $ConfirmMessage += "`r`n`r`nChanging your Live Migration Target replication configuration will cause all current instances to be disconnected from CloudEndure: you will need to reinstall the CloudEndure Agent on all the instances and data replication will restart from zero."
            }

            $WhatIfDescription = "Updated project configuration to $(ConvertTo-Json -InputObject $Config)"
            $ConfirmCaption = "Update Project Configuration"

            if ($Force -or $PSCmdlet.ShouldProcess($WhatIfDescription, $ConfirmMessage, $ConfirmCaption))
            {                
                [System.String]$UrlPath = "/projects/$ProjectId"

                $Body = ConvertTo-Json -InputObject $ConfigObject
                Write-Verbose -Message "Sending updated config`r`n$Body"
                
                try {
                    $Result = Invoke-CERequest -Path $UrlPath -Method Patch -Body $Body -Session $Session -ExpectedResponseCode 200

                    if ($PassThru)
                    {
                        Write-Output -InputObject $Result
                    }
                }
                catch [Exception] {
                    throw "There was an issue updating the project: $($_.Exception.Message)"
                }
            }
        }
        else
        {
            Write-Warning -Message "No updated configuration properties specified."
        }
    }

    End {
    }
}

Function Remove-CEProject {
    <#
        .SYNOPSIS
            Deletes a project and all sub-resources including cloud assets other than launched target machines.
 
        .DESCRIPTION
            Deletes a project and all sub-resources including cloud assets other than launched target machines.
 
        .PARAMETER ProjectId
            The Id of the project to delete.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .EXAMPLE
            Remove-CEProject -ProjectId 0cd58880-2ba0-469c-95f6-ed851f034145
 
            Deletes the specified project.
 
        .INPUTS
            None or System.Guid
 
        .OUTPUTS
            None
         
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 11/19/2019
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "HIGH")]
    [OutputType()]
    Param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [Switch]$Force,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {

    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.DefaultProjectId
        }

        # Build the confirmation messages with warnings about updates to source and destination
        $ConfirmMessage = @"
The action you are about to perform is destructive!"
 
All sub-resources including cloud assets other than currently launched target machines will be deleted.
"@


        $WhatIfDescription = "Deleted project $ProjectId"
        $ConfirmCaption = "Delete Project"

        if ($Force -or $PSCmdlet.ShouldProcess($WhatIfDescription, $ConfirmMessage, $ConfirmCaption))
        {
            [System.String]$Path = "/projects/$ProjectId"

            try {
            $Result = Invoke-CERequest -Path $Path -Method Delete -Session $Session -ExpectedResponseCode 204
            }
            catch [Exception] {
                throw "There was an issue deleting the project: $($_.Exception.Message)"
            }
        }        
    }

    End {
    }
}

#endregion

#region Cloud Credentials

Function New-CECloudCredential {
    <#
        .SYNOPSIS
            Provide the credentials with which to access the cloud API.
 
        .DESCRIPTION
            Provide the credentials with which to access the cloud API.
 
        .PARAMETER Credential
            The credential conifguration to create.
 
            The configuration schema is as follows:
            {
              "publicKey": "string",
              "name": "string",
              "cloudId": "string",
              "privateKey": "string",
              "accountIdentifier": "string"
            }
 
        .PARAMETER Path
            The path to the json file that contains the credential data.
 
        .PARAMETER PublicKey
            AWS Only. The public part of the Cloud credentials.
 
        .PARAMETER Name
            An optional (can be empty), user provided, descriptive name.
 
        .PARAMETER CloudId
            The name of the cloud to create the credentials for.
 
        .PARAMETER PrivateKey,
            Cloud credentials secret. For AWS - The secret access key, For GCP - The private key in JSON format, For Azure - The certificate file.
 
        .PARAMETER AccountIdentifier.
            Azure & GCP Only. Cloud account identifier. For GCP - The project ID, For Azure - The subscription ID.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .PARAMETER PassThru
            If specified, the new cloud credential configuration is passed to the pipeline.
 
        .EXAMPLE
            New-CECloudCredential -PublicKey AKIA12341234 -CloudId AWS -PrivateKey asdfghhoitreq+ -Name "MyAWSCreds"
 
            Creates new AWS credentials for CE to use.
 
        .INPUTS
            None or System.Collections.Hashtable
 
        .OUTPUTS
            None or System.Management.Automation.PSCustomObject
             
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/22/2019
    #>

    [CmdletBinding()]
    [OutputType()]
    Param(
        [Parameter(ParameterSetName = "Credential", ValueFromPipeline = $true, Mandatory = $true, Position = 0)]
        [ValidateNotNull()]
        [System.Collections.Hashtable]$Credential,

        [Parameter(ParameterSetName = "Path", Mandatory = $true)]
        [ValidateScript({
            Test-Path -Path $_
        })]
        [System.String]$Path,

        [Parameter(ParameterSetName = "Individual")]
        [ValidateNotNullOrEmpty()]
        [System.String]$PublicKey,

        [Parameter(ParameterSetName = "Individual")]
        [System.String]$Name,

        [Parameter(Mandatory = $true, ParameterSetName = "Individual")]
        [ValidateSet("AWS", "VCENTER", "GCP", "Azure")]
        [System.String]$CloudId,

        [Parameter(Mandatory = $true, ParameterSetName = "Individual")]
        [ValidateNotNullOrEmpty()]
        [System.String]$PrivateKey,

        [Parameter(ParameterSetName = "Individual")]
        [ValidateNotNullOrEmpty()]
        [System.String]$AccountIdentifier,

        [Parameter()]
        [Switch]$PassThru,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        $CredentialObject = [PSCustomObject]@{}

        switch ($PSCmdlet.ParameterSetName)
        {
            "Path" {
                $CredentialObject = Get-Content -Path $Path -Raw | ConvertFrom-Json
                break
            }
            "Credential" {
                $CredentialObject = [PSCustomObject]$Credential
                break
            }
            default {

                if ($PSBoundParameters.ContainsKey("PrivateKey"))
                {
                    $PSBoundParameters["PrivateKey"] = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($PrivateKey))
                }

                $Id = $script:CloudIds[$CloudId]

                # This is going to take all of the parameters supplied, put them into a hash table, and then create a json
                # document that is the blueprint
                $Credential = Convert-ParametersToHashtable -Parameters (Get-Command -Name $PSCmdlet.MyInvocation.InvocationName).Parameters `
                    -ParameterSetName $PSCmdlet.ParameterSetName `
                    -BoundParameters $PSBoundParameters 

                $CredentialObject = [PSCustomObject]$Credential

                $CredentialObject.CloudId = $Id
            }
        }

        [System.String]$UrlPath = "/cloudCredentials"
            
        $Body = ConvertTo-Json -InputObject $CredentialObject
        Write-Verbose -Message "New CE CloudCredentials:`n$Body)"
            
        try {
            $Result = Invoke-CERequest -Path $UrlPath -Method Post -Body $Body -Session $Session -ExpectedResponseCode 201

            $script:Sessions[$SessionInfo.User.Username].DefaultCloudCredentialsId = $Result.Id

            if ($PassThru)
            {
                Write-Output -InputObject $Result
            }
        }
        catch [Exception] {
            throw "There was an issue creating the new cloud credentials: $($_.Exception.Message)"
        }
    }

    End {
    }
}

Function Set-CECloudCredential {
    <#
        .SYNOPSIS
            Updates cloud credentials for CE to use in the target environment.
 
        .DESCRIPTION
            This cmdlet updates credentials that CloudEndure will utilize to launch resources in the target environment.
 
        .PARAMETER Credential
            The credential conifguration to create.
 
            The configuration schema is as follows:
            {
              "publicKey": "string",
              "name": "string",
              "cloudId": "string",
              "privateKey": "string",
              "accountIdentifier": "string"
            }
 
        .PARAMETER PublicKey
            AWS Only. The public part of the Cloud credentials.
 
        .PARAMETER Name
            An optional (can be empty), user provided, descriptive name.
 
        .PARAMETER CloudId
            The GUID Id of the cloud to create the credentials for. If this is not specified, the current cloud is used.
 
        .PARAMETER PrivateKey,
            Cloud credentials secret. For AWS - The secret access key, For GCP - The private key in JSON format, For Azure - The certificate file.
 
        .PARAMETER AccountIdentifier.
            Azure & GCP Only. Cloud account identifier. For GCP - The project ID, For Azure - The subscription ID.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .PARAMETER PassThru
            If specified, the updated cloud credential configuration is passed to the pipeline.
 
        .EXAMPLE
            Set-CECloudCredential -PublicKey AKIAPUTJUST34HYMMDRE -PrivateKey g3t89hLRcAhhq67KB8LNdx2C+9twO49uvajFF1Wa -Name "UpdatedAWSCreds"
 
            This sets new credentials for the current CE account.
 
        .INPUTS
            None
 
        .OUTPUTS
            None or System.Management.Automation.PSCustomObject
 
            The JSON representation of the returned object:
            {
              "id": "string",
              "publicKey": "string",
              "accountIdentifier": "string",
              "cloud": "string",
              "name": "string"
            }
             
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 11/19/2019
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "HIGH")]
    [OutputType([PSCustomObject])]
    Param(
        # This Id can be an empty GUID for on-premises
        [Parameter()]
        [System.Guid]$Id = [System.Guid]::Empty,

        [Parameter(ParameterSetName = "Credential", ValueFromPipeline = $true, Position = 0)]
        [ValidateNotNull()]
        [System.Collections.Hashtable]$Credential,

        [Parameter(ParameterSetName = "Individual")]
        [ValidateNotNullOrEmpty()]
        [System.String]$PublicKey,

        [Parameter(ParameterSetName = "Individual")]
        [System.String]$Name,

        [Parameter(ParameterSetName = "Individual", Mandatory = $true)]
        [ValidateSet("AWS", "Azure", "GCP", "On-Premises")]
        [System.String]$CloudId,

        [Parameter(ParameterSetName = "Individual")]
        [ValidateNotNullOrEmpty()]
        [System.String]$PrivateKey,

        [Parameter(ParameterSetName = "Individual")]
        [ValidateNotNullOrEmpty()]
        [System.String]$AccountIdentifier,

        [Parameter()]
        [Switch]$PassThru,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty,

        [Parameter()]
        [Switch]$Force
    )

    Begin {
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        if (-not $PSBoundParameters.ContainsKey("Id"))
        {
            $Id = $SessionInfo.DefaultCloudCredentialsId
        }

        [System.String]$UrlPath = "/cloudCredentials/$Id"

        if ($PSCmdlet.ParameterSetName -ine "Credential")
        {
            $Credential = Convert-ParametersToHashtable -Parameters (Get-Command -Name $PSCmdlet.MyInvocation.InvocationName).Parameters `
                    -ParameterSetName $PSCmdlet.ParameterSetName `
                    -RuntimeParameterDictionary $RuntimeParameterDictionary `
                    -BoundParameters $PSBoundParameters 

            if ($Credential.ContainsKey("cloudId"))
            {
                $Credential["cloudId"] = $script:CloudIds[$Credential["cloudId"]]
            }
            
            if ($Credential.ContainsKey("privateKey"))
            {
                $Credential["privateKey"] = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Credential["privateKey"]))
            }
        }

        if (-not $Credential.ContainsKey("cloudId"))
        {
            [System.Collections.Hashtable]$SessSplat = @{
                "Session" = $Session
            }

            $CurrentCreds = Get-CECloudCredential -Current @SessSplat
            $Credential.Add("cloudId", $CurrentCreds.Cloud)
        }

        $ConfirmMessage = "Are you sure you want to update the cloud credentials?"
        $WhatIfDescription = "Updated credentials."
        $ConfirmCaption = "Update CE Credentials"

        if ($Force -or $PSCmdlet.ShouldProcess($WhatIfDescription, $ConfirmMessage, $ConfirmCaption))
        {
            $Body = ConvertTo-Json -InputObject $Credential
            Write-Verbose -Message "Sending updated config:`r`n $Body"
            
            try {
                $Result = Invoke-CERequest -Path $UrlPath -Method Patch -Session $Session -Body $Body -ExpectedResponseCode 200

                Write-Verbose -Message "Successfully updated cloud credentials."

                if ($PassThru)
                {
                    Write-Output -InputObject $Result
                }
            }
            catch [Exception] {
                throw "There was an issue updating the cloud credentials: $($_.Exception.Message)"
            }
        }
    }

    End {
    }
}

Function Get-CECloudCredential {
    <#
        .SYNOPSIS
            Returns information about cloudCredentials in the account.
 
        .DESCRIPTION
            This cmdlet returns information about cloudCredentials in the account. If an Id is specified, the information specific to that Id is returned, otherwise the credentials are listed.
 
        .PARAMETER Offset
            With which item to start (0 based).
 
        .PARAMETER Limit
            A number specifying how many entries to return between 0 and 1500 (defaults to 1500).
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
             
        .EXAMPLE
            Get-CECloudCredential -Current
 
            Retrieves the cloud credentials associated with the current account.
 
        .EXAMPLE
            Get-CECloudCredential -Id 9f620e77-3f2e-4df3-bc37-ec4ee736d92f
 
            Get the cloud credential associated with the provided Id.
 
        .EXAMPLE
            Get-CECloudCredential -Limit 10
         
            Retrieves the first 10 cloud credentials in the current account.
 
        .INPUTS
            None or System.Guid
 
        .OUTPUTS
           System.Management.Automation.PSCustomObject or System.Management.Automation.PSCustomObject[]
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 11/19/2019
    #>

    [CmdletBinding(DefaultParameterSetName = "List")]
    [OutputType([System.Management.Automation.PSCustomObject], [System.Management.Automation.PSCustomObject[]])]
    Param(
        # This parameter can be specified as the empty GUID for on-premises
        [Parameter(ValueFromPipeline = $true, Mandatory = $true, Position = 0, ParameterSetName = "Get")]
        [System.Guid]$Id,

        [Parameter(ParameterSetName = "List")]
        [ValidateRange(0, [System.UInt32]::MaxValue)]
        [System.UInt32]$Offset = 0,

        [Parameter(ParameterSetName = "List")]
        [ValidateRange(0, 1500)]
        [System.UInt32]$Limit = 1500,

        [Parameter(ParameterSetName = "Current")]
        [Switch]$Current,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {        
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        [System.String]$UrlPath = "/cloudCredentials"

        switch ($PSCmdlet.ParameterSetName)
        {
            "Get" {
                $UrlPath += "/$($Id.ToString())"

                break
            }
            "Current" {
                $Id = $SessionInfo.DefaultCloudCredentialsId
                $UrlPath += "/$($Id.ToString())"

                break
            }
            "List" {
                if ($Offset -gt 0 -or $Limit -lt 1500)
                {
                    $QueryString = [System.String]::Empty

                    if ($Offset -gt 0)
                    {
                        $QueryString += "&offset=$Offset"
                    }

                    if ($Limit -lt 1500)
                    {
                        $QueryString += "&limit=$Limit"
                    }

                    # Remove the first character which is an unecessary ampersand
                    $UrlPath += "?$($QueryString.Substring(1))"
                }
                break
            }
            default {
                Write-Warning -Message "Encountered an unknown parameter set $($PSCmdlet.ParameterSetName)."
                break
            }
        }

        try {
            $Result = Invoke-CERequest -Path $UrlPath -Method Get -Session $Session -ExpectedResponseCode 200

            if ($PSCmdlet.ParameterSetName -ieq "List")
            {
                Write-Output -InputObject $Result.Items
            }
            else
            {
                Write-Output -InputObject $Result
            }
        }
        catch [Exception] {
            throw "There was an issue getting the cloud credentials: $($_.Exception.Message)"
        }
    }

    End {
    }
}

#endregion

#region Cloud

Function Get-CECloud {
    <#
        .SYNOPSIS
            Gets information about the available clouds to use with CloudEndure
 
        .DESCRIPTION
            The cmdlet retrieves information about a cloud or lists the available clouds if no Id is specified.
 
        .PARAMETER Id
            The Id of the cloud to retrieve. If no Id is specified, all available clouds are returned.
 
        .PARAMETER Name
            The name of the cloud to retrieve. If no name is specified, all available clouds are returned.
 
        .PARAMETER Offset
            With which item to start (0 based).
 
        .PARAMETER Limit
            A number specifying how many entries to return between 0 and 1500 (defaults to 1500).
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .EXAMPLE
            Get-CECloud
 
            Retrieves all of the available clouds.
 
        .EXAMPLE
            Get-CECloud -Id 4c7b3582-9e73-4866-858a-8e1ac6e818b3
 
            Retrieves information about the cloud with the specified Id, which is AWS.
 
        .EXAMPLE
            Get-CECloud -Name AWS
 
            Retrieves information about the AWS cloud.
 
        .INPUTS
            None
 
        .OUPUTS
            System.Management.Automation.PSCustomObject or System.Management.Automation.PSCustomObject[]
 
            This is a JSON representation of the return array:
            [
                {
                  "id": "string",
                  "roles": [
                    "SOURCE"
                  ],
                  "name": "AWS"
                }
            ]
             
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/16/2019
    #>

    [CmdletBinding(DefaultParameterSetName = "List")]
    [OutputType([System.Management.Automation.PSCustomObject], [System.Management.Automation.PSCustomObject[]])]
    Param (
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = "GetById")]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$Id = [System.Guid]::Empty,

        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = "GetByName")]
        [ValidateNotNullOrEmpty()]
        [System.String]$Name = [System.String]::Empty,

        [Parameter(ParameterSetName = "List")]
        [ValidateRange(0, [System.UInt32]::MaxValue)]
        [System.UInt32]$Offset = 0,

        [Parameter(ParameterSetName = "List")]
        [ValidateRange(0, 1500)]
        [System.UInt32]$Limit = 1500,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }

    Process {
        [System.String]$Path = "/clouds"
        [System.Int32]$ResultCount = 0
        [System.Boolean]$Found = $false

        do {
            [System.String]$QueryString = "?offset=$Offset&limit=$Limit"
            [System.String]$TempUri = "$Path$QueryString"

            Write-Verbose -Message "Querying clouds from $Offset to $($Offset + $Limit)."

            try {
                $Result = Invoke-CERequest -Path $TempUri -Method Get -Session $Session -ExpectedResponseCode 200
            }
            catch [Exception] {
                throw "There was an issue listing the clouds: $($_.Exception.Message)"
            }

            [PSCustomObject[]]$Content = $Result.Items
            $ResultCount = $Result.Items.Length

            switch -wildcard ($PSCmdlet.ParameterSetName)
            {
                "Get*" {

                    $Filter = {}
                    if ($PSCmdlet.ParameterSetName -eq "GetById")
                    {
                        $Filter = {$_.Id -ieq $Id.ToString()}
                    }
                    else
                    {
                        $Filter = {$_.name -ieq $Name}
                    }

                    $Cloud = $Content | Where-Object $Filter

                    if ($Cloud -ne $null)
                    {
                        Write-Output -InputObject ([PSCustomObject]($Cloud | Select-Object -First 1))
                        $Found = $true
                        break
                    }
                    else
                    {
                        $Offset += $Limit
                    }

                    break
                }
                "List" {
                    Write-Output -InputObject $Content
                    $ResultCount = $Limit - 1 # Make sure we break the do loop
                    break
                }
                default {
                    throw "Encountered an unknown parameter set $($PSCmdlet.ParameterSetName)."
                }
            }

        } while ($ResultCount -ge $Limit)

        if ($PSCmdlet.ParameterSetName -like "Get*" -and -not $Found)
        {
            throw "The cloud with Id $Id was not found."
        }
    }

    End {
    }
}

Function Get-CECloudRegion {
    <#
        .SYNOPSIS
            Gets information about the available destination cloud regions.
 
        .DESCRIPTION
            The cmdlet retrieves information about a region in the target cloud or if no Id is specified, all available regions in the target cloud.
            This information includes the available regions, their subnets, security groups, IAM instance profiles, available instance types, and KMS keys.
 
        .PARAMETER Id
            The Id of the region to retrieve. If no Id is specified, all available regions in the target cloud are returned.
 
        .PARAMETER Offset
            With which item to start (0 based).
 
        .PARAMETER Limit
            A number specifying how many entries to return between 0 and 1500 (defaults to 1500).
 
        .PARAMETER Current
            Gets the region information about a project's target region. This defaults to the default project.
 
        .PARAMETER Target
            Gets the region information about a project's target region. This defaults to the default project.
 
        .PARAMETER CloudCredentials
            UUID of the credentials to use. In case of on-premise, you should use the null UUID "00000000-0000-0000-0000-000000000000". If this is not specified, it defaults to the cloud credentials acquired at logon.
 
        .PARAMETER ProjectId
            The project Id to use if you are trying to get access about a source or destination region. This defaults to the current project retrieved from the login.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
             
        .EXAMPLE
            Get-CECloudRegion
 
            Retrieves the details of all regions the destination cloud environment.
 
        .EXAMPLE
            Get-CECloudRegion -Id 47d842b8-ebfa-4695-90f8-fb9ab686c708
 
            Retrieves details of the region identified with the supplied Guid.
 
        .EXAMPLE
            Get-CECloudRegion -Current
             
            Retrieves details about the current target region.
 
        .INPUTS
            System.Guid
 
        .OUTPUTS
           System.Management.Automation.PSCustomObject or System.Management.Automation.PSCustomObject[]
 
            This is a JSON representation of the output array:
            [
                {
                  "subnets": [
                    {
                      "subnetId": "string",
                      "networkId": "string",
                      "name": "string"
                    }
                  ],
                  "placementGroups": [
                    "string"
                  ],
                  "scsiAdapterTypes": [
                    "string"
                  ],
                  "instanceTypes": [
                    "string"
                  ],
                  "zones": [
                    "string"
                  ],
                  "volumeEncryptionKeys": [
                    "string"
                  ],
                  "cloud": "string",
                  "securityGroups": [
                    {
                      "networkId": "string",
                      "securityGroupId": "string",
                      "name": "string"
                    }
                  ],
                  "logicalLocations": [
                    {
                      "locationId": "string",
                      "name": "string"
                    }
                  ],
                  "staticIps": [
                    "string"
                  ],
                  "maxCpusPerMachine": 0,
                  "networkInterfaces": [
                    {
                      "subnetId": "string",
                      "name": "string",
                      "privateIp": "string"
                    }
                  ],
                  "computeLocations": [
                    {
                      "isEncryptionSupported": true,
                      "locationId": "string",
                      "name": "string"
                    }
                  ],
                  "name": "string",
                  "storageLocations": [
                    {
                      "locationId": "string",
                      "name": "string"
                    }
                  ],
                  "iamRoles": [
                    "string"
                  ],
                  "id": "string",
                  "maxCoresPerMachineCpu": 0,
                  "dedicatedHosts": [
                    "string"
                  ],
                  "networkAdapterTypes": [
                    "string"
                  ],
                  "maxMbRamPerMachine": 0
                }
            ]
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/16/2019
    #>

    [CmdletBinding(DefaultParameterSetName = "List")]
    [OutputType([PSCustomObject], [PSCustomObject[]])]
    Param(
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = "Get")]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$Id = [System.Guid]::Empty,

        <# TODO
        [Parameter(ValueFromPipeline = $true, Mandatory = $true, Position = 0, ParameterSetName = "GetByName")]
        [ValidateNotNullOrEmpty()]
        [System.String]$Name = [System.String]::Empty,
        #>


        [Parameter(ParameterSetName = "List")]
        [ValidateRange(0, [System.UInt32]::MaxValue)]
        [System.UInt32]$Offset = 0,

        [Parameter(ParameterSetName = "List")]
        [ValidateRange(0, 1500)]
        [System.UInt32]$Limit = 1500,

        [Parameter(ParameterSetName = "GetTarget")]
        [Switch]$Target,

        [Parameter(ParameterSetName = "GetSource")]
        [Switch]$Source,

        [Parameter()]
        [System.Guid]$CloudCredentials = [System.Guid]::Empty,

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {

    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        $Splat = @{}

        if (-not [System.String]::IsNullOrEmpty($Session))
        {
            $Splat.Add("Session", $Session)
        }

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo["DefaultProjectId"]
        }

        # Check to see if they were specified since an empty GUID means on-premises
        if (-not $PSBoundParameters.ContainsKey("CloudCredentials"))
        {
            $CloudCredentials = $SessionInfo["DefaultCloudCredentialsId"]
        }

        [System.String]$Path = "/cloudCredentials/$CloudCredentials/regions"

        switch ($PSCmdlet.ParameterSetName)
        {
            "Get" {
                $Path += "/$($Id.ToString())"

                break
            }
            "GetTarget" {    
                $Project = Get-CEProject -Id $ProjectId @Splat

                if ([System.String]::IsNullOrEmpty($Project.ReplicationConfiguration))
                {
                    throw  "The project $($Project.Name) ($ProjectId) does not have a replication configuration yet, so the target region cannot be determined."
                }

                $ReplConfig = Get-CEReplicationConfiguration -Id $Project.ReplicationConfiguration -ProjectId $ProjectId @Splat
                $Path += "/$($ReplConfig.Region.ToString())"

                break
            }
            "GetSource" {
                $Project = Get-CEProject -Id $ProjectId @Splat
                $Id = $Project.sourceRegion
                
                if ($Id -ne $script:CloudIds["Generic"])
                {
                    $Path += "/$($Id.ToString())"
                }
                else 
                {
                    return [PSCustomObject]@{
                        "cloud" = "GENERIC"; 
                        "iamRoles" = @(); 
                        "id" = $script:CloudIds["Generic"]; 
                        "instanceTypes" = @(); 
                        "name" = "Generic"; 
                        "placementGroups" = @(); 
                        "securityGroups" = @(); 
                        "staticIps" = @(); 
                        "subnets" = @(@{"name" = "Default"}); 
                        "volumeEncryptionKeys" = @("Default")
                    }
                }

                break
            }
            "List" {
                if ($Offset -gt 0 -or $Limit -lt 1500)
                {
                    $QueryString = [System.String]::Empty

                    if ($Offset -gt 0)
                    {
                        $QueryString += "&offset=$Offset"
                    }

                    if ($Limit -lt 1500)
                    {
                        $QueryString += "&limit=$Limit"
                    }

                    # Remove the first character which is an unecessary ampersand
                    $Path += "?$($QueryString.Substring(1))"
                }
                break
            }
            default {
                Write-Warning -Message "Encountered an unknown parameter set $($PSCmdlet.ParameterSetName)."
                break
            }
        }
        try {
            $Result = Invoke-CERequest -Path $Path -Method Get -Session $Session -ExpectedResponseCode 200
        
            if ($PSCmdlet.ParameterSetName -eq "List")
            {
                $Return += $Result.Items

                Write-Output -InputObject $Result.Items
            }
            else 
            {
                Write-Output -InputObject $Result
            }
        }
        catch [Exception] {
            throw "There was an issue listing the cloud regions: $($_.Exception.Message)"
        }
    }

    End {
    }
}

Function Get-CETargetCloudRegion {
    <#
        .SYNOPSIS
            Gets information about the destination cloud environment.
 
        .DESCRIPTION
            The cmdlet retrieves information about the target/destination cloud environment.
 
        .PARAMETER CloudCredentials
            UUID of the credentials to use. In case of on-premise, you should use the null UUID "00000000-0000-0000-0000-000000000000". If this is not specified, it defaults to the cloud credentials acquired at logon.
 
        .PARAMETER ProjectId
            The project Id of whose target cloud you want to retrieve. This defaults to the current project retrieved from the login.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
             
        .EXAMPLE
            Get-CETargetCloudRegion
 
            Retrieves the details of the destination cloud environment.
 
        .INPUTS
            None
 
        .OUTPUTS
           System.Management.Automation.PSCustomObject
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 11/21/2019
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    Param(
        [Parameter()]
        [System.Guid]$CloudCredentials = [System.Guid]::Empty,

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }

    Process {
        $Splat = @{}

        if (-not [System.String]::IsNullOrEmpty($Session))
        {
            $Splat.Add("Session", $Session)
        }

        if ($PSBoundParameters.ContainsKey("CloudCredentials"))
        {
            $Splat.Add("CloudCredentials", $CloudCredentials)
        }

        if ($PSBoundParameters.ContainsKey("ProjectId"))
        {
            $Splat.Add("ProjectId", $ProjectId)
        }

        Write-Output -InputObject (Get-CECloudRegion -Target @Splat)
    }

    End {
    }
}

Function Get-CESourceCloudRegion {
    <#
        .SYNOPSIS
            Gets information about the source cloud environment.
 
        .DESCRIPTION
            The cmdlet retrieves information about the source cloud environment.
 
        .PARAMETER CloudCredentials
            UUID of the credentials to use. In case of on-premise, you should use the null UUID "00000000-0000-0000-0000-000000000000". If this is not specified, it defaults to the cloud credentials acquired at logon.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
             
        .EXAMPLE
            Get-CESourceCloud
 
            Retrieves the details of the source cloud environment.
 
        .INPUTS
            None
 
        .OUTPUTS
           System.Management.Automation.PSCustomObject
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 11/21/2019
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    Param(
        [Parameter()]
        [System.Guid]$CloudCredentials = [System.Guid]::Empty,

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {

    }

    Process {
        $Splat = @{}

        if (-not [System.String]::IsNullOrEmpty($Session))
        {
            $Splat.Add("Session", $Session)
        }

        if ($PSBoundParameters.ContainsKey("CloudCredentials"))
        {
            $Splat.Add("CloudCredentials", $CloudCredentials)
        }

        if ($PSBoundParameters.ContainsKey("ProjectId"))
        {
            $Splat.Add("ProjectId", $ProjectId)
        }

        Write-Output -InputObject (Get-CECloudRegion -Source @Splat)
    }

    End {
    }
}

Function Remove-CERegion {
    <#
        .SYNOPSIS
            Deletes a vCenter region.
 
        .DESCRIPTION
            Deletes a vCenter region.
 
        .PARAMETER Id
            The Id of the region to delete.
 
        .PARAMETER CloudCredentialsId
            The id of the cloud credentials used to delete the region. In case of on-premise, you should use the null UUID "00000000-0000-0000-0000-000000000000".
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
       
        .EXAMPLE
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 12/18/2019
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")]
    [OutputType()]
    Param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [System.Guid]$Id,

        [Parameter()]
        [ValidateNotNull()]
        [System.Guid]$CloudCredentialsId = [System.Guid]::Empty,

        [Parameter()]
        [Switch]$Force,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }

    Process {
        [System.String]$UrlPath = "/cloudCredentials/$($CloudCredentialsId.ToString())/regions/$($Id.ToString())"

        $ConfirmMessage = @"
You are about to delete the vCenter region $Id.
"@


        $WhatIfDescription = "Deleted region $Id"
        $ConfirmCaption = "Delete region $Id."

        if ($Force -or $PSCmdlet.ShouldProcess($WhatIfDescription, $ConfirmMessage, $ConfirmCaption))
        {
            try {
                Invoke-CERequest -Path $UrlPath -Method Delete -Session $Session -ExpectedResponseCode 200
            }
            catch [Exception] {
                throw "There was an issue deleting the region: $($_.Exception.Message)"
            }
        }
    }

    End {
    }
}

Function Set-CERegion {
    <#
        .SYNOPSIS
            Updates a vCenter region, limited to renaming.
 
        .DESCRIPTION
            Updates a vCenter region, limited to renaming.
 
        .PARAMETER Id
            The Id of the region to update.
 
        .PARAMETER CloudCredentialsId
            The id of the cloud credentials used to update the region. In case of on-premise, you should use the null UUID "00000000-0000-0000-0000-000000000000".
 
        .PARAMETER NewName
            The new name of the region.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
       
        .EXAMPLE
            Set-CERegion -Id $Id -NewName "OnPremColo1"
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 12/18/2019
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")]
    [OutputType()]
    Param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [System.Guid]$Id,

        [Parameter()]
        [ValidateNotNull()]
        [System.Guid]$CloudCredentialsId = [System.Guid]::Empty,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]$NewName,

        [Parameter()]
        [Switch]$Force,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }

    Process {
        [System.String]$UrlPath = "/cloudCredentials/$($CloudCredentialsId.ToString())/regions/$($Id.ToString())"

        $ConfirmMessage = @"
You are about to update the vCenter region $Id.
"@


        $WhatIfDescription = "Updated region $Id"
        $ConfirmCaption = "Update region $Id."

        if ($Force -or $PSCmdlet.ShouldProcess($WhatIfDescription, $ConfirmMessage, $ConfirmCaption))
        {
            try {
                [System.String]$Body = @{"name" = $NewName} | ConvertTo-Json -Compress
                Invoke-CERequest -Path $UrlPath -Method Patch -Body $Body -Session $Session -ExpectedResponseCode 200
            }
            catch [Exception] {
                throw "There was an issue updating the region: $($_.Exception.Message)"
            }
        }
    }

    End {
    }
}

#endregion

#region Machines

Function Get-CEMachine {
     <#
        .SYNOPSIS
           Gets a list of CE machines in an account or a specific CE machine.
 
        .DESCRIPTION
            The cmdlet lists all of the CE machines in the account if no Id is provided. If an Id is provided, then that specific machine is fetched.
 
            When listing machines, only currently replicating machines are returned by default.
 
        .PARAMETER Id
            The Id of the instance to get. If this is not specified, all instances are returned.
 
        .PARAMETER Offset
            With which item to start (0 based).
 
        .PARAMETER Limit
            A number specifying how many entries to return between 0 and 1500 (defaults to 1500).
 
        .PARAMETER IgnoreMachineStatus
            Returns all machines in the project regardless of replications status. If not specified only currently replicating machines will be returned.
 
        .PARAMETER Types
            Control which machines are returned, either source or replica. If you do not specify this parameter only source machines are returned.
     
        .PARAMETER ProjectId
            The project Id to use to retrieve the details. This defaults to the current project retrieved from the login.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
             
        .EXAMPLE
            Get-CEMachine
 
            Lists all of the CE machines in the account.
 
        .EXAMPLE
            Get-CEMachine -Id 9f620e77-3f2e-4df3-bc37-ec4ee736d92f
 
            Gets details for the machine specified.
 
        .INPUTS
            None or System.Guid
 
        .OUTPUTS
           System.Management.Automation.PSCustomObject or System.Management.Automation.PSCustomObject[]
 
            This is a JSON representation of the returned array:
            [
                {
                  "sourceProperties": {
                    "name": "string",
                    "installedApplications": {
                      "items": [
                        {
                          "applicationName": "string"
                        }
                      ],
                      "lastUpdatedDateTime": "2019-11-22T14:34:36Z"
                    },
                    "disks": [
                      {
                        "isProtected": true,
                        "name": "string",
                        "size": 0
                      }
                    ],
                    "machineCloudState": "string",
                    "publicIps": [
                      "string"
                    ],
                    "memory": 0,
                    "os": "string",
                    "cpu": [
                      {
                        "cores": 0,
                        "modelName": "string"
                      }
                    ],
                    "runningServices": {
                      "items": [
                        {
                          "serviceName": "string"
                        }
                      ],
                      "lastUpdatedDateTime": "2019-11-22T14:34:36Z"
                    },
                    "machineCloudId": "string"
                  },
                  "replicationInfo": {
                    "rescannedStorageBytes": 0,
                    "backloggedStorageBytes": 0,
                    "lastConsistencyDateTime": "2019-11-22T14:34:36Z",
                    "nextConsistencyEstimatedDateTime": "2019-11-22T14:34:36Z",
                    "initiationStates": {
                      "items": [
                        {
                          "steps": [
                            {
                              "status": "NOT_STARTED",
                              "message": "string",
                              "name": "WAITING_TO_INITIATE_REPLICATION"
                            }
                          ],
                          "startDateTime": "2019-11-22T14:34:36Z"
                        }
                      ],
                      "estimatedNextAttemptDateTime": "2019-11-22T14:34:36Z"
                    },
                    "replicatedStorageBytes": 0,
                    "lastSeenDateTime": "2019-11-22T14:34:36Z",
                    "totalStorageBytes": 0
                  },
                  "license": {
                    "startOfUseDateTime": "2019-11-22T14:34:36Z",
                    "licenseId": "string"
                  },
                  "tags": [
                    "string"
                  ],
                  "restoreServers": [
                    "string"
                  ],
                  "fromPointInTime": {
                    "id": "string",
                    "dateTime": "2019-11-22T14:34:36Z"
                  },
                  "replicationStatus": "STOPPED",
                  "replica": "string",
                  "id": "string",
                  "replicationConfiguration": {
                    "volumeEncryptionKey": "string",
                    "replicationTags": [
                      {
                        "key": "string",
                        "value": "string"
                      }
                    ],
                    "useLowCostDisks": true,
                    "subnetHostProject": "string",
                    "replicationServerType": "string",
                    "disablePublicIp": true,
                    "computeLocationId": "string",
                    "subnetId": "string",
                    "logicalLocationId": "string",
                    "bandwidthThrottling": 0,
                    "storageLocationId": "string",
                    "useDedicatedServer": true,
                    "zone": "string",
                    "replicatorSecurityGroupIDs": [
                      "string"
                    ],
                    "usePrivateIp": true,
                    "proxyUrl": "string",
                    "volumeEncryptionAllowed": true,
                    "archivingEnabled": true,
                    "objectStorageLocation": "string",
                    "stagingDisks": [
                      {
                        "iops": 0,
                        "type": "DEFAULT",
                        "name": "string"
                      }
                    ]
                  },
                  "lifeCycle": {
                    "lastTestLaunchDateTime": "2019-11-22T14:34:36Z",
                    "connectionEstablishedDateTime": "2019-11-22T14:34:36Z",
                    "agentInstallationDateTime": "2019-11-22T14:34:36Z",
                    "lastCutoverDateTime": "2019-11-22T14:34:36Z",
                    "lastRecoveryLaunchDateTime": "2019-11-22T14:34:36Z"
                  },
                  "isAgentInstalled": true
                }
            ]
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 11/21/2019
    #>

    [CmdletBinding(DefaultParameterSetName = "List")]
    [OutputType([PSCustomObject], [PSCustomObject[]])]
    Param(
        [Parameter(ValueFromPipeline = $true, Position = 0, ParameterSetName = "Get", Mandatory = $true)]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$Id = [System.Guid]::Empty,

        [Parameter(ParameterSetName = "List")]
        [ValidateRange(0, [System.UInt32]::MaxValue)]
        [System.UInt32]$Offset = 0,

        [Parameter(ParameterSetName = "List")]
        [ValidateRange(0, 1500)]
        [System.UInt32]$Limit = 1500,

        [Parameter(ParameterSetName = "List")]
        [Switch]$IgnoreMachineStatus,

        [Parameter(ParameterSetName = "List")]
        [ValidateNotNull()]
        [System.String[]]$Types = @(),

        [Parameter()]

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.ProjectId
        }

        [System.String]$UrlPath = "/projects/$ProjectId/machines"

        switch ($PSCmdlet.ParameterSetName)
        {
            "Get" {
                $UrlPath += "/$($Id.ToString())"

                break
            }
            "List" {
                $QueryString = [System.String]::Empty

                if ($Offset -gt 0)
                {
                    $QueryString += "&offset=$Offset"
                }

                if ($Limit -lt 1500)
                {
                    $QueryString += "&limit=$Limit"
                }

                if ($IgnoreMachineStatus)
                {
                    $QueryString += "&all=true"
                }

                if ($Types.Count -gt 0)
                {
                    $QueryString += "&types=$([System.String]::Join(",", $Types))"
                }

                # Remove the first character which is an unecessary ampersand
                if ($QueryString.Length -gt 0)
                {
                    $QueryString = $QueryString.Substring(1);
                }
                
                $UrlPath += "?$QueryString"
                
                break
            }
            default {
                throw "Encountered an unknown parameter set $($PSCmdlet.ParameterSetName)."
                break
            }
        }

        try {
            $Result = Invoke-CERequest -Path $UrlPath -Method Get -Session $Session -ExpectedResponseCode 200

            if ($PSCmdlet.ParameterSetName -ieq "List") 
            {
                Write-Output -InputObject $Result.Items
            }
            else 
            {
                Write-Output -InputObject $Result
            }
        }
        catch [Exception] {
            throw "There was an issue retrieving CE machines: $($_.Exception.Message)"
        }
    }

    End {
    }
}

Function Set-CEMachine {
    <#
        .SYNOPSIS
            Updates a machine's configuration.
 
        .DESCRIPTION
            This cmdlet updates a machine's configuration. It only accepts Launch time updates. This means only the sub-properties of the "lifeCycle" property can be updated.
 
        .PARAMETER InstanceId
            The Id of the machine to update.
 
        .PARAMETER Path
            The path to the file containing the config.
 
        .PARAMETER Config
            The configuration settings to update on the CE machine. This hashtable can include the following key values, but will only accept Launch time updates:
 
            {
              "sourceProperties": {
                "name": "string",
                "installedApplications": {
                  "items": [
                    {
                      "applicationName": "string"
                    }
                  ],
                  "lastUpdatedDateTime": "2019-11-22T14:34:36Z"
                },
                "disks": [
                  {
                    "isProtected": true,
                    "name": "string",
                    "size": 0
                  }
                ],
                "machineCloudState": "string",
                "publicIps": [
                  "string"
                ],
                "memory": 0,
                "os": "string",
                "cpu": [
                  {
                    "cores": 0,
                    "modelName": "string"
                  }
                ],
                "runningServices": {
                  "items": [
                    {
                      "serviceName": "string"
                    }
                  ],
                  "lastUpdatedDateTime": "2019-11-22T14:34:36Z"
                },
                "machineCloudId": "string"
              },
              "replicationInfo": {
                "rescannedStorageBytes": 0,
                "backloggedStorageBytes": 0,
                "lastConsistencyDateTime": "2019-11-22T14:34:36Z",
                "nextConsistencyEstimatedDateTime": "2019-11-22T14:34:36Z",
                "initiationStates": {
                  "items": [
                    {
                      "steps": [
                        {
                          "status": "NOT_STARTED",
                          "message": "string",
                          "name": "WAITING_TO_INITIATE_REPLICATION"
                        }
                      ],
                      "startDateTime": "2019-11-22T14:34:36Z"
                    }
                  ],
                  "estimatedNextAttemptDateTime": "2019-11-22T14:34:36Z"
                },
                "replicatedStorageBytes": 0,
                "lastSeenDateTime": "2019-11-22T14:34:36Z",
                "totalStorageBytes": 0
              },
              "license": {
                "startOfUseDateTime": "2019-11-22T14:34:36Z",
                "licenseId": "string"
              },
              "tags": [
                "string"
              ],
              "restoreServers": [
                "string"
              ],
              "fromPointInTime": {
                "id": "string",
                "dateTime": "2019-11-22T14:34:36Z"
              },
              "replicationStatus": "STOPPED",
              "replica": "string",
              "id": "string",
              "replicationConfiguration": {
                "volumeEncryptionKey": "string",
                "replicationTags": [
                  {
                    "key": "string",
                    "value": "string"
                  }
                ],
                "useLowCostDisks": true,
                "subnetHostProject": "string",
                "replicationServerType": "string",
                "disablePublicIp": true,
                "computeLocationId": "string",
                "subnetId": "string",
                "logicalLocationId": "string",
                "bandwidthThrottling": 0,
                "storageLocationId": "string",
                "useDedicatedServer": true,
                "zone": "string",
                "replicatorSecurityGroupIDs": [
                  "string"
                ],
                "usePrivateIp": true,
                "proxyUrl": "string",
                "volumeEncryptionAllowed": true,
                "archivingEnabled": true,
                "objectStorageLocation": "string",
                "stagingDisks": [
                  {
                    "iops": 0,
                    "type": "DEFAULT",
                    "name": "string"
                  }
                ]
              },
              "lifeCycle": {
                "lastTestLaunchDateTime": "2019-11-22T14:34:36Z",
                "connectionEstablishedDateTime": "2019-11-22T14:34:36Z",
                "agentInstallationDateTime": "2019-11-22T14:34:36Z",
                "lastCutoverDateTime": "2019-11-22T14:34:36Z",
                "lastRecoveryLaunchDateTime": "2019-11-22T14:34:36Z"
              },
              "isAgentInstalled": true
            }
 
        .PARAMETER LastTestLaunchDateTime
            The new last test launch datetime.
 
        .PARAMETER LastCutoverDateTime
            The new last cutover datetime.
 
        .PARAMETER LastRecoveryLaunchDateTime
            The new last recovery launch datetime.
 
        .EXAMPLE
            Set-CEMachine -InstanceId 114b110e-12a3-48d4-b731-90ab3fccdf22 -LastTestLaunchDateTime (Get-Date)
 
        .INPUTS
            System.Guid
 
        .OUTPUTS
            None or PSCustomObject
 
            This is a JSON representation of the returned object:
 
            {
              "sourceProperties": {
                "name": "string",
                "disks": [
                  {
                    "isProtected": true,
                    "name": "string",
                    "size": 0
                  }
                ],
                "machineCloudState": "string",
                "publicIps": [
                  "string"
                ],
                "memory": 0,
                "os": "string",
                "cpu": [
                  {
                    "cores": 0,
                    "modelName": "string"
                  }
                ],
                "machineCloudId": "string"
              },
              "replicationInfo": {
                "lastConsistencyDateTime": "2017-10-04T15:34:44Z",
                "nextConsistencyEstimatedDateTime": "2017-10-04T15:34:44Z",
                "rescannedStorageBytes": 0,
                "backloggedStorageBytes": 0,
                "initiationStates": {
                  "items": [
                    {
                      "steps": [
                        {
                          "status": "NOT_STARTED",
                          "message": "string",
                          "name": "WAITING_TO_INITIATE_REPLICATION"
                        }
                      ],
                      "startDateTime": "2017-10-04T15:34:44Z"
                    }
                  ],
                  "estimatedNextAttemptDateTime": "2017-10-04T15:34:44Z"
                },
                "replicatedStorageBytes": 0,
                "totalStorageBytes": 0
              },
              "license": {
                "startOfUseDateTime": "2017-10-04T15:34:44Z",
                "licenseId": "string"
              },
              "id": "string",
              "replicationStatus": "STOPPED",
              "replica": "string",
              "lifeCycle": {
                "lastTestLaunchDateTime": "2017-10-04T15:34:44Z",
                "connectionEstablishedDateTime": "2017-10-04T15:34:44Z",
                "agentInstallationDateTime": "2017-10-04T15:34:44Z",
                "lastCutoverDateTime": "2017-10-04T15:34:44Z",
                "lastRecoveryLaunchDateTime": "2017-10-04T15:34:44Z"
              },
              "isAgentInstalled": true
            }
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 11/21/2019
             
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "HIGH")]
    [OutputType([System.Management.Automation.PSCustomObject])]
    Param(
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]
        [System.Guid]$InstanceId,

        [Parameter(Mandatory = $true, ParameterSetName = "Config")]
        [ValidateNotNull()]
        [System.Collections.Hashtable]$Config = @{},

        [Parameter(Mandatory = $true, ParameterSetName = "Path")]
        [ValidateScript({
            Test-Path -Path $_
        })]
        [System.String]$Path,

        [Parameter(ParameterSetName = "Property")]
        [ValidateNotNull()]
        [System.DateTime]$LastTestLaunchDateTime,

        [Parameter(ParameterSetName = "Property")]
        [ValidateNotNull()]
        [System.DateTime]$LastCutoverDateTime,

        [Parameter(ParameterSetName = "Property")]
        [ValidateNotNull()]
        [System.DateTime]$LastRecoveryLaunchDateTime,

        [Parameter()]
        [Switch]$PassThru,

        [Parameter()]
        [Switch]$Force,

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.ProjectId
        }

        $MachineObject = [PSCustomObject]@{}

        switch ($PSCmdlet.ParameterSetName)
        {
            "Path" {
                $MachineObject = Get-Content -Path $Path | ConvertFrom-Json
                break
            }
            "Config" {
                $MachineObject = [PSCustomObject]$Config
                break
            }
            "Property" {
                $Temp = Convert-ParametersToHashtable -Parameters (Get-Command -Name $PSCmdlet.MyInvocation.InvocationName).Parameters `
                    -ParameterSetName $PSCmdlet.ParameterSetName `
                    -RuntimeParameterDictionary $RuntimeParameterDictionary `
                    -BoundParameters $PSBoundParameters 

                $Config = @{ lifecycle = @{}}

                foreach ($Item in $Temp.GetEnumerator())
                {
                    $Config["lifecycle"].Add($Item.Key, ([System.DateTime]$Item.Value).ToString("yyyy-MM-ddTHH:mm:ssZ"))
                }

                $MachineObject = [PSCustomObject]$Config

                break
            }
        }

        [System.String]$UrlPath = "/projects/$ProjectId/machines/$InstanceId"
        [System.String]$Body = ConvertTo-Json -InputObject $MachineObject

        $ConfirmMessage = "Are you sure you want to update the CE machine configuration for machine $InstanceId`?"

        $WhatIfDescription = "Updated configuration to $Body"
        $ConfirmCaption = "Update CE Machine Configuration"

        if ($Force -or $PSCmdlet.ShouldProcess($WhatIfDescription, $ConfirmMessage, $ConfirmCaption))
        {
            Write-Verbose -Message "Sending updated machine config:`r`n$Body"

            try {
                $Result = Invoke-CERequest -Path $UrlPath -Method Patch -Session $Session -ExpectedResponseCode 200

                if ($PassThru)
                {
                    Write-Output -InputObject $Result
                }
            }
            catch [Exception] {
                throw "There was an issue updating CE machine $InstanceId`: $($_.Exception.Message)"
            }
        }
    }

    End {

    }
}

Function Remove-CEMachine {
    <#
        .SYNOPSIS
           Removes a machine from CloudEndure and initiates an uninstall of the agent on the source machine.
 
        .DESCRIPTION
            The cmdlet uninstalls the CloudEndure agent on the source instance, causes data replication to stop, and the instance will no longer appear in the CloudEndure Console.
 
            All cloud artifacts associated with those machines with the exception of launched target machines are deleted.
 
        .PARAMETER Ids
            The Ids of the instances to remove from CloudEndure.
 
        .PARAMETER ProjectId
            The project Id to use to retrieve the details. This defaults to the current project retrieved from the login.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
             
        .EXAMPLE
            Remove-CEMachine -Ids e0dc06ba-86b5-4c4c-b25b-20a68089c797 -Force
 
            Removes the CE instance with the specified Id and bypasses the confirm dialog.
 
        .EXAMPLE
            Remove-CEMachine -Ids @(e0dc06ba-86b5-4c4c-b25b-20a68089c797, b1df0696-8da5-4648-b2cc-222aa89c800) -Force
 
            Removes the CE instancea with the specified Ids and bypasses the confirm dialog.
 
        .INPUTS
            System.Guid[]
 
        .OUTPUTS
           None
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/21/2019
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")]
    [OutputType()]
    Param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [System.Guid[]]$Ids = @(),

        [Parameter()]
        [Switch]$Force,

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.DefaultProjectId
        }

        [System.String]$Path = "/projects/$ProjectId/machines"

        [System.String]$Body = ConvertTo-Json -InputObject @{"machineIDs" = $Ids}

        $ConfirmMessage = @"
You are about to uninstall the CloudEndure Agent from $($Ids.Length) Source instance$(if ($Ids.Length -gt 1) { "s" }).
 
This will cause data replication to stop and the instance$(if ($Ids.Length -gt 1) { "s" }) will no longer appear in the CloudEndure Console.
"@


        $WhatIfDescription = "Deleted CE Instances $([System.String]::Join(",", $Ids))"
        $ConfirmCaption = "Delete CE Instance $([System.String]::Join(",", $Ids))"

        if ($Force -or $PSCmdlet.ShouldProcess($WhatIfDescription, $ConfirmMessage, $ConfirmCaption))
        {
            try {
                $Result = Invoke-CERequest -Path $Path -Method Delete -Session $Session -Body $Body -ExpectedResponseCode 204
            }
            catch [Exception]
            {
                throw "There was an issue removing the CE machines: $($_.Exception.Message)"
            }        
        }    
    }

    End {
    }
}

Function Get-CEMachineReplica {
    <#
        .SYNOPSIS
            Gets a target machine details.
 
        .DESCRIPTION
            This cmdlet retrieves information about a target replica instance. If the id is not found, a 404 response is returned.
 
        .PARAMETER Id
            The Id of the replica instance to get.
 
        .PARAMETER ProjectId
            The project Id to use to retrieve the details. This defaults to the current project retrieved from the login.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .EXAMPLE
            Get-CEMachineReplica -Id cc7ba582-9e83-4866-858a-8e1ac6e818b4
 
            Gets the replica instance specified by the provided Id.
 
        .INPUTS
            System.Guid
 
        .OUPUTS
            System.Management.Automation.PSCustomObject
 
            This is a JSON representation of the return value:
            {
              "machine": "string",
              "cloudEndureCreationDateTime": "2019-12-18T18:59:29Z",
              "name": "string",
              "pointInTime": "string",
              "machineCloudState": "string",
              "publicIps": [
                "string"
              ],
              "regionId": "string",
              "id": "string",
              "machineCloudId": "string"
            }
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 12/18/2019
    #>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSCustomObject])]
    Param(        
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$Id,

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,
        
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.ProjectId
        }

        [System.String]$UrlPath = "/projects/$ProjectId/replicas/$($Id.ToString())"
            
        try {
            $ErrorHandling = {
                Param($StatusCode, $Content, $ErrorMessage, $ReplicaId)
                
                switch ($StatusCode)
                {
                    200 {
                        Write-Output -InputObject $Content
                        break
                    }
                    404 {
                        throw "Replica Id $ReplicaId not found."
                    }
                    default {
                        throw $ErrorMessage
                    }            
                }
            }
            
            $Result = Invoke-CERequest -Path $UrlPath -Method Get -Session $Session -ErrorHandling $ErrorHandling -ErrorHandlingArgs @($Id)
            Write-Output -InputObject $Result
        }
        catch [Exception] {
            throw "There was an issue retrieving the target machine: $($_.Exception.Message)"
        }
    }

    End {
    }
}

Function Invoke-CEMachineBatchUpdate {
    <#
        .SYNOPSIS
            Batch updates multiple machines.
 
        .DESCRIPTION
            Batch updates multiple machines.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 12/18/2019
 
            Not implemented, CE documentation unclear.
    #>

    [CmdletBinding()]
    Param(
        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )
}

#endregion

#region Actions

Function New-CEInstallationToken {
    <#
        .SYNOPSIS
            Replaces the current installation token with a new one.
 
        .DESCRIPTION
            The cmdlet creates a new installation token and invalidates the old one.
 
        .PARAMETER PassThru
            If specified, the new installation token will be returned to the pipeline.
 
        .PARAMETER ProjectId
            The project Id to use to retrieve the details. This defaults to the current project retrieved from the login.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
             
        .EXAMPLE
            $Token = New-CEInstallationToken -PassThru
 
            Invalidates the old installation token and creates a new one, which is passed back to the pipeline.
 
        .INPUTS
            None
 
        .OUTPUTS
           None or System.String
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 12/18/2019
    #>

    [CmdletBinding()]
    [OutputType([System.String])]
    Param(
        [Parameter()]
        [Switch]$PassThru,

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {        
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.ProjectId
        }

        [System.String]$UrlPath = "/projects/$ProjectId/replaceAgentInstallationToken"


        try {
    
            $Result = Invoke-CERequest -Path $UrlPath -Session $Session -Method Get -ExpectedResponseCode 200

            Write-Verbose -Message "Successfully replaced token."

            if ($PassThru) 
            {
                Write-Output -InputObject $Result.AgentInstallationToken
            }
        }
        catch [Exception] {
            throw "There was an issue replacing the installation token: $($_.Exception.Message)"        
        }
    }

    End {
    }
}

Function Get-CEInstallationToken {
    <#
        .SYNOPSIS
            Gets the current installation token.
 
        .DESCRIPTION
            The cmdlet gets the current installation token.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
             
        .EXAMPLE
            $Token = Get-CEInstallationToken
 
            Gets the current installation token.
 
        .INPUTS
            None
 
        .OUTPUTS
           System.String
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 12/18/2019
    #>

    [CmdletBinding()]
    [OutputType([System.String])]
    Param(
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {        
    }

    Process {
        $Splat = @{}

        if (-not [System.String]::IsNullOrEmpty($Session))
        {
            $Splat.Add("Session", $Session)
        }

        $Result = Get-CEAccountExtendedInfo @Splat

        Write-Output -InputObject $Result.AgentInstallationToken
    }

    End {
    }
}

Function Start-CEDataReplication {
    <#
        .SYNOPSIS
           Starts data replication for specified instances.
 
        .DESCRIPTION
            The cmdlet starts data replication for specified instances.
 
            If invalid IDs are provided, they are ignored and identified in the return data.
             
        .PARAMETER Ids
            The Ids of the instances to start replication for.
 
        .PARAMETER ProjectId
            The project Id to use to retrieve the details. This defaults to the current project retrieved from the login.
 
        .PARAMETER PassThru
            If specified, the cmdlet will return updated instance information as well as a list of invalid IDs.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
             
        .EXAMPLE
            Start-CEReplication -Ids e0dc06ba-86b5-4c4c-b25b-20a68089c797 -Force
 
            Starts replication for the specified instance.
 
        .INPUTS
            System.Guid[]
 
        .OUTPUTS
           PSCustomObject
 
            This is a JSON representation of the returned data:
            {
              "items": [
                {
                  "sourceProperties": {
                    "name": "string",
                    "installedApplications": {
                      "items": [
                        {
                          "applicationName": "string"
                        }
                      ],
                      "lastUpdatedDateTime": "2019-12-18T18:59:29Z"
                    },
                    "disks": [
                      {
                        "isProtected": true,
                        "name": "string",
                        "size": 0
                      }
                    ],
                    "machineCloudState": "string",
                    "publicIps": [
                      "string"
                    ],
                    "memory": 0,
                    "os": "string",
                    "cpu": [
                      {
                        "cores": 0,
                        "modelName": "string"
                      }
                    ],
                    "runningServices": {
                      "items": [
                        {
                          "serviceName": "string"
                        }
                      ],
                      "lastUpdatedDateTime": "2019-12-18T18:59:29Z"
                    },
                    "machineCloudId": "string"
                  },
                  "replicationInfo": {
                    "rescannedStorageBytes": 0,
                    "backloggedStorageBytes": 0,
                    "lastConsistencyDateTime": "2019-12-18T18:59:29Z",
                    "nextConsistencyEstimatedDateTime": "2019-12-18T18:59:29Z",
                    "initiationStates": {
                      "items": [
                        {
                          "steps": [
                            {
                              "status": "NOT_STARTED",
                              "message": "string",
                              "name": "WAITING_TO_INITIATE_REPLICATION"
                            }
                          ],
                          "startDateTime": "2019-12-18T18:59:29Z"
                        }
                      ],
                      "estimatedNextAttemptDateTime": "2019-12-18T18:59:29Z"
                    },
                    "replicatedStorageBytes": 0,
                    "lastSeenDateTime": "2019-12-18T18:59:29Z",
                    "totalStorageBytes": 0
                  },
                  "license": {
                    "startOfUseDateTime": "2019-12-18T18:59:29Z",
                    "licenseId": "string"
                  },
                  "tags": [
                    "string"
                  ],
                  "restoreServers": [
                    "string"
                  ],
                  "fromPointInTime": {
                    "id": "string",
                    "dateTime": "2019-12-18T18:59:29Z"
                  },
                  "replicationStatus": "STOPPED",
                  "replica": "string",
                  "id": "string",
                  "replicationConfiguration": {
                    "volumeEncryptionKey": "string",
                    "replicationTags": [
                      {
                        "key": "string",
                        "value": "string"
                      }
                    ],
                    "useLowCostDisks": true,
                    "subnetHostProject": "string",
                    "replicationServerType": "string",
                    "disablePublicIp": true,
                    "computeLocationId": "string",
                    "subnetId": "string",
                    "logicalLocationId": "string",
                    "bandwidthThrottling": 0,
                    "storageLocationId": "string",
                    "useDedicatedServer": true,
                    "zone": "string",
                    "replicatorSecurityGroupIDs": [
                      "string"
                    ],
                    "usePrivateIp": true,
                    "proxyUrl": "string",
                    "volumeEncryptionAllowed": true,
                    "archivingEnabled": true,
                    "objectStorageLocation": "string",
                    "stagingDisks": [
                      {
                        "iops": 0,
                        "type": "DEFAULT",
                        "name": "string"
                      }
                    ]
                  },
                  "lifeCycle": {
                    "lastTestLaunchDateTime": "2019-12-18T18:59:29Z",
                    "connectionEstablishedDateTime": "2019-12-18T18:59:29Z",
                    "agentInstallationDateTime": "2019-12-18T18:59:29Z",
                    "lastCutoverDateTime": "2019-12-18T18:59:29Z",
                    "lastRecoveryLaunchDateTime": "2019-12-18T18:59:29Z"
                  },
                  "isAgentInstalled": true
                }
              ],
              "job": {
                "status": "PENDING",
                "participatingMachines": [
                  "string"
                ],
                "log": [
                  {
                    "message": "string",
                    "logDateTime": "2019-12-18T18:59:29Z"
                  }
                ],
                "type": "CLEANUP",
                "endDateTime": "2019-12-18T18:59:29Z",
                "creationDateTime": "2019-12-18T18:59:29Z",
                "id": "string",
                "initiatedBy": "string"
              },
              "invalidMachineIDs": [
                "string"
              ]
            }
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 12/18/2019
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")]
    [OutputType([PSCustomObject])]
    Param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid[]]$Ids = @(),

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [Switch]$PassThru,

        [Parameter()]
        [Switch]$Force,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        [System.Collections.Hashtable]$Splat = @{}

        if (-not [System.String]::IsNullOrEmpty($Session))
        {
            $Splat.Add("Session", $Session)
        }

        if ($ProjectId -ne [System.Guid]::Empty)
        {
            $Splat.Add("ProjectId", $ProjectId)
        }
        else
        {
            $ProjectId = $SessionInfo.ProjectId
        }

        [System.String]$UrlPath = "/projects/$ProjectId/startReplication"

        [System.String]$Body = ConvertTo-Json -InputObject @{"machineIDs" = $Ids}

        $Target = Get-CETargetCloud @Splat | Select-Object -ExpandProperty Cloud

        $ConfirmMessage = @"
Are you sure you want to start data replication?
 
If you continue, you will begin to incur additional costs from $Target for data transfer, storage, compute and other resources.
 
(selected instances for which data replication is already started will not be affected)
"@


        $WhatIfDescription = "Started replication for CE Instances $([System.String]::Join(",", $Ids))"
        $ConfirmCaption = "Start Data Replication for $($Ids.Length) Instance$(if ($Ids.Length -gt 1) { "s" })"

        if ($Force -or $PSCmdlet.ShouldProcess($WhatIfDescription, $ConfirmMessage, $ConfirmCaption))
        {
            Write-Verbose -Message "Requesting replication for:`r`n$Body"
            
            try {
                $Result = Invoke-CERequest -Path $UrlPath -Method Post -Body $Body -Session $Session -ExpectedResponseCode 200

                if ($Result.Items -ne $null -and $Result.Items.Length -gt 0)
                {
                    if ($PassThru)
                    {
                        Write-Output -InputObject $Result
                    }
                }
                else
                {
                    Write-Warning -Message "No items were returned for successful replication stop."
                }

                if ($Result.InvalidMachineIDs -ne $null -and $Result.InvalidMachineIDs.Length -gt 0)
                {
                    Write-Warning -Message "The following ids were invalid: $([System.String]::Join(",", ($Result | Select-Object -ExpandProperty InvalidMachineIDs)))" 
                }
            }
            catch [Exception] {
                throw "There was an issue starting replication: $($_.Exception.Message)"
            }
        }
    }

    End {
    }
}

Function Stop-CEDataReplication {
    <#
        .SYNOPSIS
           Stops data replication for specified instances.
 
        .DESCRIPTION
            The cmdlet stops data replication for specified instances. The instances will remain in the console, and replication can be started from zero again.
 
            If invalid IDs are provided, they are ignored and identified in the return data.
 
        .PARAMETER Ids
            The Ids of the instances to stop replication on.
 
        .PARAMETER MoveVMDKs
            Should the replica vmdks be moved.
 
        .PARAMETER ProjectId
            The project Id to use to retrieve the details. This defaults to the current project retrieved from the login.
 
        .PARAMETER PassThru
            If specified, the cmdlet will return updated instance information as well as a list of invalid IDs.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
             
        .EXAMPLE
            Stop-CEReplication -Ids e0dc06ba-86b5-4c4c-b25b-20a68089c797 -Force
 
            Stops replication for the specified instance.
 
        .INPUTS
            System.Guid[]
 
        .OUTPUTS
            None or PSCustomObject
 
            This is a JSON representation of the returned data:
            {
              "items": [
                {
                  "sourceProperties": {
                    "name": "string",
                    "installedApplications": {
                      "items": [
                        {
                          "applicationName": "string"
                        }
                      ],
                      "lastUpdatedDateTime": "2019-12-18T18:59:29Z"
                    },
                    "disks": [
                      {
                        "isProtected": true,
                        "name": "string",
                        "size": 0
                      }
                    ],
                    "machineCloudState": "string",
                    "publicIps": [
                      "string"
                    ],
                    "memory": 0,
                    "os": "string",
                    "cpu": [
                      {
                        "cores": 0,
                        "modelName": "string"
                      }
                    ],
                    "runningServices": {
                      "items": [
                        {
                          "serviceName": "string"
                        }
                      ],
                      "lastUpdatedDateTime": "2019-12-18T18:59:29Z"
                    },
                    "machineCloudId": "string"
                  },
                  "replicationInfo": {
                    "rescannedStorageBytes": 0,
                    "backloggedStorageBytes": 0,
                    "lastConsistencyDateTime": "2019-12-18T18:59:29Z",
                    "nextConsistencyEstimatedDateTime": "2019-12-18T18:59:29Z",
                    "initiationStates": {
                      "items": [
                        {
                          "steps": [
                            {
                              "status": "NOT_STARTED",
                              "message": "string",
                              "name": "WAITING_TO_INITIATE_REPLICATION"
                            }
                          ],
                          "startDateTime": "2019-12-18T18:59:29Z"
                        }
                      ],
                      "estimatedNextAttemptDateTime": "2019-12-18T18:59:29Z"
                    },
                    "replicatedStorageBytes": 0,
                    "lastSeenDateTime": "2019-12-18T18:59:29Z",
                    "totalStorageBytes": 0
                  },
                  "license": {
                    "startOfUseDateTime": "2019-12-18T18:59:29Z",
                    "licenseId": "string"
                  },
                  "tags": [
                    "string"
                  ],
                  "restoreServers": [
                    "string"
                  ],
                  "fromPointInTime": {
                    "id": "string",
                    "dateTime": "2019-12-18T18:59:29Z"
                  },
                  "replicationStatus": "STOPPED",
                  "replica": "string",
                  "id": "string",
                  "replicationConfiguration": {
                    "volumeEncryptionKey": "string",
                    "replicationTags": [
                      {
                        "key": "string",
                        "value": "string"
                      }
                    ],
                    "useLowCostDisks": true,
                    "subnetHostProject": "string",
                    "replicationServerType": "string",
                    "disablePublicIp": true,
                    "computeLocationId": "string",
                    "subnetId": "string",
                    "logicalLocationId": "string",
                    "bandwidthThrottling": 0,
                    "storageLocationId": "string",
                    "useDedicatedServer": true,
                    "zone": "string",
                    "replicatorSecurityGroupIDs": [
                      "string"
                    ],
                    "usePrivateIp": true,
                    "proxyUrl": "string",
                    "volumeEncryptionAllowed": true,
                    "archivingEnabled": true,
                    "objectStorageLocation": "string",
                    "stagingDisks": [
                      {
                        "iops": 0,
                        "type": "DEFAULT",
                        "name": "string"
                      }
                    ]
                  },
                  "lifeCycle": {
                    "lastTestLaunchDateTime": "2019-12-18T18:59:29Z",
                    "connectionEstablishedDateTime": "2019-12-18T18:59:29Z",
                    "agentInstallationDateTime": "2019-12-18T18:59:29Z",
                    "lastCutoverDateTime": "2019-12-18T18:59:29Z",
                    "lastRecoveryLaunchDateTime": "2019-12-18T18:59:29Z"
                  },
                  "isAgentInstalled": true
                }
              ],
              "job": {
                "status": "PENDING",
                "participatingMachines": [
                  "string"
                ],
                "log": [
                  {
                    "message": "string",
                    "logDateTime": "2019-12-18T18:59:29Z"
                  }
                ],
                "type": "CLEANUP",
                "endDateTime": "2019-12-18T18:59:29Z",
                "creationDateTime": "2019-12-18T18:59:29Z",
                "id": "string",
                "initiatedBy": "string"
              },
              "invalidMachineIDs": [
                "string"
              ]
            }
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 12/18/2019
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")]
    [OutputType([PSCustomObject])]
    Param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid[]]$Ids = @(),

        [Parameter()]
        [System.Boolean]$MoveVMDKs,

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [Switch]$PassThru,

        [Parameter()]
        [Switch]$Force,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.ProjectId
        }

        [System.String]$UrlPath = "/projects/$ProjectId/stopReplication"

        $Content = @{"machineIDs" = $Ids}

        if ($PSBoundParameters.ContainsKey("MoveVMDKs"))
        {
            $Content.Add("MoveVmdks", $MoveVMDKs)
        }

        [System.String]$Body = ConvertTo-Json -InputObject $Content

        $ConfirmMessage = @"
Are you sure you want to stop data replication?
 
If you continue, all replicated data for $(if ($Ids.Length -gt 1) { "these instances" } else { "this instance" }) will be purged and you will no longer be able to launch Target instances for either testing purposes or for Cutover.
 
$(if ($Ids.Length -gt 1) { "These instances" } else { "This instance" }) will still appear in this Console and you will be able to restart data replication for $(if ($Ids.Length -gt 1) { "them" } else { "it" }) whenever you wish, however data replication will then begin from zero.
 
(selected instances for which data replication is already stopped will not be affected)
"@


        $WhatIfDescription = "Stopped replication for CE Instances $([System.String]::Join(",", $Ids))"
        $ConfirmCaption = "Stop Data Replication for $($Ids.Length) Instance$(if ($Ids.Length -gt 1) { "s" })"

        if ($Force -or $PSCmdlet.ShouldProcess($WhatIfDescription, $ConfirmMessage, $ConfirmCaption))
        {
            Write-Verbose -Message "Requesting to stop replication for:`r`n$Body"
            

            try {
                $ErrorHandling = {
                    Param($StatusCode, $Content, $ErrorMessage)
            
                    switch ($StatusCode)
                    {
                        200 {
                            Write-Output -InputObject $Content
                            break
                        }
                        409 {
                            throw "Another job is already running in this project."
                        }
                        429 {
                            throw "Too many jobs are running for this project."
                        }
                        default {
                            throw $ErrorMessage
                        }            
                    }
                }

                $Result = Invoke-CERequest -Path $UrlPath -Method Post -Body $Body -Session $Session -ErrorHandling $ErrorHandling

                if ($Result.Items -ne $null -and $Result.Items.Length -gt 0)
                {
                    Write-Verbose -Message "Replication successfully stopped for machine(s) $([System.String]::Join(",", ($Result | Select-Object -ExpandProperty Items | Select-Object -ExpandProperty Id)))."

                    if ($PassThru)
                    {
                        Write-Output -InputObject $Result
                    }
                }
                else
                {
                    Write-Warning -Message "No items were returned for successfull replication stop."
                }

                if ($Result.InvalidMachineIDs -ne $null -and $Result.InvalidMachineIDs.Length -gt 0)
                {
                    Write-Warning -Message "The following ids were invalid: $([System.String]::Join(",", ($Result | Select-Object -ExpandProperty InvalidMachineIDs)))" 
                }
            }
            catch [Exception] {
                throw "There was an issue stopping replication: $($_.Exception.Message)"
            }            
        }
    }

    End {
    }
}

Function Suspend-CEDataReplication {
    <#
        .SYNOPSIS
           Pauses data replication for specified instances.
 
        .DESCRIPTION
            The cmdlet pauses data replication for specified instances. The instances will remain in the console, and replication can be started again.
 
            If invalid IDs are provided, they are ignored and identified in the return data.
 
        .PARAMETER Ids
            The Ids of the instances to pause replication on.
 
        .PARAMETER ProjectId
            The project Id that the specified machines are in. This defaults to the current project retrieved from the login.
 
        .PARAMETER PassThru
            If specified, the cmdlet will return updated instance information as well as a list of invalid IDs.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
             
        .EXAMPLE
            Suspend-CEReplication -Ids e0dc06ba-86b5-4c4c-b25b-20a68089c797 -Force
 
            Pauses replication for the specified instance.
 
        .INPUTS
            System.Guid[]
 
        .OUTPUTS
            None or PSCustomObject
 
            This is a JSON representation of the returned data:
            {
              "items": [
                {
                  "sourceProperties": {
                    "name": "string",
                    "installedApplications": {
                      "items": [
                        {
                          "applicationName": "string"
                        }
                      ],
                      "lastUpdatedDateTime": "2019-12-18T18:59:29Z"
                    },
                    "disks": [
                      {
                        "isProtected": true,
                        "name": "string",
                        "size": 0
                      }
                    ],
                    "machineCloudState": "string",
                    "publicIps": [
                      "string"
                    ],
                    "memory": 0,
                    "os": "string",
                    "cpu": [
                      {
                        "cores": 0,
                        "modelName": "string"
                      }
                    ],
                    "runningServices": {
                      "items": [
                        {
                          "serviceName": "string"
                        }
                      ],
                      "lastUpdatedDateTime": "2019-12-18T18:59:29Z"
                    },
                    "machineCloudId": "string"
                  },
                  "replicationInfo": {
                    "rescannedStorageBytes": 0,
                    "backloggedStorageBytes": 0,
                    "lastConsistencyDateTime": "2019-12-18T18:59:29Z",
                    "nextConsistencyEstimatedDateTime": "2019-12-18T18:59:29Z",
                    "initiationStates": {
                      "items": [
                        {
                          "steps": [
                            {
                              "status": "NOT_STARTED",
                              "message": "string",
                              "name": "WAITING_TO_INITIATE_REPLICATION"
                            }
                          ],
                          "startDateTime": "2019-12-18T18:59:29Z"
                        }
                      ],
                      "estimatedNextAttemptDateTime": "2019-12-18T18:59:29Z"
                    },
                    "replicatedStorageBytes": 0,
                    "lastSeenDateTime": "2019-12-18T18:59:29Z",
                    "totalStorageBytes": 0
                  },
                  "license": {
                    "startOfUseDateTime": "2019-12-18T18:59:29Z",
                    "licenseId": "string"
                  },
                  "tags": [
                    "string"
                  ],
                  "restoreServers": [
                    "string"
                  ],
                  "fromPointInTime": {
                    "id": "string",
                    "dateTime": "2019-12-18T18:59:29Z"
                  },
                  "replicationStatus": "STOPPED",
                  "replica": "string",
                  "id": "string",
                  "replicationConfiguration": {
                    "volumeEncryptionKey": "string",
                    "replicationTags": [
                      {
                        "key": "string",
                        "value": "string"
                      }
                    ],
                    "useLowCostDisks": true,
                    "subnetHostProject": "string",
                    "replicationServerType": "string",
                    "disablePublicIp": true,
                    "computeLocationId": "string",
                    "subnetId": "string",
                    "logicalLocationId": "string",
                    "bandwidthThrottling": 0,
                    "storageLocationId": "string",
                    "useDedicatedServer": true,
                    "zone": "string",
                    "replicatorSecurityGroupIDs": [
                      "string"
                    ],
                    "usePrivateIp": true,
                    "proxyUrl": "string",
                    "volumeEncryptionAllowed": true,
                    "archivingEnabled": true,
                    "objectStorageLocation": "string",
                    "stagingDisks": [
                      {
                        "iops": 0,
                        "type": "DEFAULT",
                        "name": "string"
                      }
                    ]
                  },
                  "lifeCycle": {
                    "lastTestLaunchDateTime": "2019-12-18T18:59:29Z",
                    "connectionEstablishedDateTime": "2019-12-18T18:59:29Z",
                    "agentInstallationDateTime": "2019-12-18T18:59:29Z",
                    "lastCutoverDateTime": "2019-12-18T18:59:29Z",
                    "lastRecoveryLaunchDateTime": "2019-12-18T18:59:29Z"
                  },
                  "isAgentInstalled": true
                }
              ],
              "job": {
                "status": "PENDING",
                "participatingMachines": [
                  "string"
                ],
                "log": [
                  {
                    "message": "string",
                    "logDateTime": "2019-12-18T18:59:29Z"
                  }
                ],
                "type": "CLEANUP",
                "endDateTime": "2019-12-18T18:59:29Z",
                "creationDateTime": "2019-12-18T18:59:29Z",
                "id": "string",
                "initiatedBy": "string"
              },
              "invalidMachineIDs": [
                "string"
              ]
            }
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 12/18/2019
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")]
    [OutputType([PSCustomObject])]
    Param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid[]]$Ids = @(),

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [Switch]$PassThru,

        [Parameter()]
        [Switch]$Force,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session
            
        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.ProjectId
        }

        [System.String]$UrlPath = "/projects/$ProjectId/pauseReplication"

        [System.String]$Body = ConvertTo-Json -InputObject @{"machineIDs" = $Ids}

            $ConfirmMessage = @"
Are you sure you want to pause data replication?
 
$(if ($Ids.Length -gt 1) { "These instances" } else { "This instance" }) will still appear in this Console and you will be able to restart data replication for $(if ($Ids.Length -gt 1) { "them" } else { "it" }) whenever you wish.
 
(selected instances for which data replication is already paused or stopped will not be affected)
"@


        $WhatIfDescription = "Paused replication for CE Instances $([System.String]::Join(",", $Ids))"
        $ConfirmCaption = "Pause Data Replication for $($Ids.Length) Instance$(if ($Ids.Length -gt 1) { "s" })"

        if ($Force -or $PSCmdlet.ShouldProcess($WhatIfDescription, $ConfirmMessage, $ConfirmCaption))
        {
            Write-Verbose -Message "Requesting to pause replication for:`r`n$Body"

            try {
                $Result = Invoke-CERequest -Path $UrlPath -Method Post -Body $Body -Session $Session -ExpectedResponseCode 200

                if ($Result.Items -ne $null -and $Result.Items.Length -gt 0)
                {
                    Write-Verbose -Message "Replication successfully paused for machine(s) $([System.String]::Join(",", ($Result | Select-Object -ExpandProperty Items | Select-Object -ExpandProperty Id)))."

                    if ($PassThru)
                    {
                        Write-Output -InputObject $Result
                    }
                }
                else
                {
                    Write-Warning -Message "No items were returned for successfull replication stop."
                }

                if ($Result.InvalidMachineIDs -ne $null -and $Result.InvalidMachineIDs.Length -gt 0)
                {
                    Write-Warning -Message "The following ids were invalid: $([System.String]::Join(",", ($Result | Select-Object -ExpandProperty InvalidMachineIDs)))" 
                }
            }
            catch [Exception] {
                throw "There was an issue pausing replication: $($_.Exception.Message)"
            }
        }
    }

    End {
    }
}

Function Get-CEJobs {
    <#
        .SYNOPSIS
           Gets the log information from active CE jobs.
 
        .DESCRIPTION
            The cmdlet lists all of log information about a currently running CE jobs.
 
        .PARAMETER ProjectId
            The project Id to use to retrieve the details. This defaults to the current project retrieved from the login.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
             
        .EXAMPLE
            Get-CEJobs
 
            Gets the log data for active jobs.
 
        .INPUTS
            None
 
        .OUTPUTS
            System.Management.Automation.PSCustomObject
 
            This is a JSON representation of the returned object:
            {
              "status": "PENDING",
              "participatingMachines": [
                "string"
              ],
              "log": [
                {
                  "message": "string",
                  "logDateTime": "2019-12-18T18:59:29Z"
                }
              ],
              "type": "CLEANUP",
              "endDateTime": "2019-12-18T18:59:29Z",
              "creationDateTime": "2019-12-18T18:59:29Z",
              "id": "string",
              "initiatedBy": "string"
            }
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 12/18/2019
    #>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSCustomObject], [System.Management.Automation.PSCustomObject[]])]
    Param(
        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$Id = [System.Guid]::Empty,

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {        
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.ProjectId
        }

        [System.String]$UrlPath = "/projects/$ProjectId/jobs"

        if ($Id -ne [System.Guid]::Empty)
        {
            $UrlPath += "/$($Id.ToString())"
        }
        
        try {
            $Result = Invoke-CERequest -Path $UrlPath -Method Get -Session $Session -ExpectedResponseCode 200

            if ($Id -ne [System.Guid]::Empty)
            {
                Write-Output -InputObject $Result.Items
            }
            else
            {
                Write-Output -InputObject $Result
            }
        }
        catch [Exception] {
            throw "There was an issue getting the jobs: $($_.Exception.Message)"
        }
    }

    End {
    }
}

Function Invoke-CEReplicaCleanup {
    <#
        .SYNOPSIS
            Spawns a cleanup job to remove the specified target machines from the cloud.
 
        .DESCRIPTION
            Spawns a cleanup job to remove the specified target machines from the cloud.
 
        .PARAMETER Ids
            The list of replica IDs to delete (corresponding to the 'replica' field in the machine object).
 
        .PARAMETER ProjectId
            The project Id to use to retrieve the details. This defaults to the current project retrieved from the login.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .PARAMETER PassThru
            Returns the job created by the cmdlet.
 
        .EXAMPLE
            Invoke-CEReplicaCleanup -Ids @("3a0b0738-e46d-489b-a735-5856a1eafb49")
 
            Begins a cleanup job for the replica instance indicated by the supplied id.
 
        .INPUTS
            System.Guid[]
 
        .OUTPUTS
            None or System.Management.Automation.PSCustomObject
 
            This is a JSON representation of the returned value:
            {
              "status": "PENDING",
              "participatingMachines": [
                "string"
              ],
              "log": [
                {
                  "message": "string",
                  "logDateTime": "2019-12-18T18:59:29Z"
                }
              ],
              "type": "CLEANUP",
              "endDateTime": "2019-12-18T18:59:29Z",
              "creationDateTime": "2019-12-18T18:59:29Z",
              "id": "string",
              "initiatedBy": "string"
            }
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 12/18/2019
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "HIGH")]
    [OutputType()]
    Param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid[]]$Ids = @(),

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [Switch]$PassThru,

        [Parameter()]
        [Switch]$Force,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }
    
    Process {
         $SessionInfo = Get-CESessionOrDefault -Session $Session

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.ProjectId
        }

        [System.String]$UrlPath = "/projects/$ProjectId/replicas"

        [System.String]$Body = ConvertTo-Json -InputObject @{"replicaIDs" = $Ids}

        $ConfirmMessage = @"
This cleanup will remove the specified target machines from the cloud.
"@

        $WhatIfDescription = "Cleaned up $($Ids.Length) instance$(if ($Ids.Length -gt 1) { "s" })."
        $ConfirmCaption = "Cleanup $($Ids.Length) Instance$(if ($Ids.Length -gt 1) { "s" })"

        if ($Force -or $PSCmdlet.ShouldProcess($WhatIfDescription, $ConfirmMessage, $ConfirmCaption))
        {
            try {
                $ErrorHandling = {
                    Param($StatusCode, $Content, $ErrorMessage)
        
                    switch ($StatusCode)
                    {
                        202 {
                            Write-Output -InputObject $Content
                            break
                        }
                        400 {
                            throw "Another job is already running in this project."
                        }                        
                        default {
                            throw $ErrorMessage
                        }            
                    }
                }

                $Result = Invoke-CERequest -Path $UrlPath -Method Delete -Session $Session -ErrorHandling $ErrorHandling

                Write-Verbose -Message "Cleanup successfully started."

                if ($PassThru)
                {
                    Write-Output -InputObject $Result
                }
            }
            catch [Exception] {
                throw "There was an issue launching the cleanup: $($_.Exception.Message)"
            }
        }        
    }

    End {
    }
}

Function Invoke-CEReverseReplication {
    <#
        .SYNOPSIS
            Reverses replication for a DR project.
 
        .DESCRIPTION
            This cmdlet reverses the direction of replication for a DR project.
 
        .PARAMETER ProjectId
            The project Id to reverse replication for. This defaults to the current project retrieved from the login.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .PARAMETER PassThru
            Returns the job created by the cmdlet.
 
        .EXAMPLE
            Invoke-CEReverseReplication
 
            Reverses replication for the current project stored in the session.
 
        .INPUTS
            System.Guid
 
        .OUTPUTS
            None or System.Management.Automation.PSCustomObject
 
            This is a JSON representation of the returned value:
            {
              "status": "PENDING",
              "type": "TEST",
              "id": "string",
              "log": [
                {
                  "message": "string",
                  "logDateTime": "2017-09-10T14:19:39Z"
                }
              ]
            }
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 12/18/2019
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "HIGH")]
    [OutputType([System.Management.Automation.PSCustomObject])]
    Param(
        [Parameter(Position = 0, ValueFromPipeline = $true)]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [Switch]$Force,

        [Parameter()]
        [Switch]$PassThru,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.ProjectId
        }

        [System.String]$UrlPath = "/projects/$ProjectId/reverseReplication"

        $ConfirmMessage = @"
This will reverse the direction of replication for the project $ProjectId.
"@

        $WhatIfDescription = "Reversed replication for project $ProjectId."
        $ConfirmCaption = "Reverse Replication For Project"

        if ($Force -or $PSCmdlet.ShouldProcess($WhatIfDescription, $ConfirmMessage, $ConfirmCaption))
        {
            try {
                $ErrorHandling = {
                    Param($StatusCode, $Content, $ErrorMessage)
        
                    switch ($StatusCode)
                    {
                        200 {
                            Write-Output -InputObject $Content
                            break
                        }
                        400 {
                            throw "There is already another job running, $Content"
                        }    
                        422 {
                            throw "The project cannot be reversed."
                        }
                        default {
                            throw $ErrorMessage
                        }            
                    }
                }

                $Result = Invoke-CERequest -Path $UrlPath -Method Post -Body "" -Session $Session -ErrorHandling $ErrorHandling
            }
            catch [Exception] {
                throw "There was an issue reversing replication: $($_.Exception.Message)"
            }
        }
    }

    End {
    }
}

Function Invoke-CELaunchTargetMachine {
    <#
        .SYNOPSIS
            Launch target machines for test, recovery or cutover.
 
        .DESCRIPTION
            This cmdlet launches target machines for test, recovery or cutover.
 
        .PARAMETER LaunchType
            Specify TEST, RECOVERY, or CUTOVER.
 
        .PARAMETER Ids
            The Ids of the CE machines to launch. Specifying this parameter will use the latest point in time for each machine.
 
        .PARAMETER Items
            An array of Id and PointInTime Id objects (specified as Hashtables). If the point in time Id is omitted, the latest point in time is used. For example:
 
            @(
                @{"Id" = "3a0b0738-e46d-489b-a735-5856a1eafb49"; "PointInTimeId" = "4b73848c-9d02-41e5-ac17-a79cf0e3b919"}
                @{"Id" = "f1ffcc9b-8988-4273-acba-96828ef24b0e"}
            )
 
        .PARAMETER ProjectId
            The project Id that the machines are part of. This defaults to the current project retrieved from the login.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .EXAMPLE
            Invoke-CELaunchTargetMachine -LaunchType TEST -Ids @("f1ffcc9b-8988-4273-acba-96828ef24b0e")
 
            Launches a new test instance for migration for the specified machine id.
 
        .INPUTS
            None or System.Collections.Hashtable[]
 
        .OUTPUTS
            None or System.Management.Automation.PSCustomObject
 
            This is a JSON representation of the returned value:
            {
              "status": "PENDING",
              "participatingMachines": [
                "string"
              ],
              "log": [
                {
                  "message": "string",
                  "logDateTime": "2019-12-18T18:59:29Z"
                }
              ],
              "type": "CLEANUP",
              "endDateTime": "2019-12-18T18:59:29Z",
              "creationDateTime": "2019-12-18T18:59:29Z",
              "id": "string",
              "initiatedBy": "string"
            }
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 12/18/2019
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "HIGH")]
    [OutputType([System.Management.Automation.PSCustomObject])]
    Param(

        [Parameter(Mandatory = $true)]
        [ValidateSet("TEST", "RECOVERY", "CUTOVER", "DEBUG")]
        [System.String]$LaunchType,

        [Parameter(Mandatory = $true, ParameterSetName = "Ids")]
        [System.Guid[]]$Ids,

        [Parameter(Mandatory = $true, ParameterSetName = "Items")]
        [System.Collections.Hashtable[]]$Items,

        [Parameter()]
        [ValidateNotNull()]
        [System.String[]]$PostBootDebugScripts = @(),

        [Parameter()]
        [ValidateNotNull()]
        [System.String[]]$PreBootDebugScripts = @(),

        [Parameter(Position = 0, ValueFromPipeline = $true)]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [Switch]$Force,

        [Parameter()]
        [Switch]$PassThru,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session
            
        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.ProjectId
        }

        if ($PSCmdlet.ParameterSetName -eq "Ids")
        {
            $Items = @()
            foreach ($Item in $Ids)
            {
                $Items += @{"Id" = $Item}
            }
        }

        [System.Collections.Hashtable]$Request = @{ "launchType" = $LaunchType; "items" = $Items}

        if ($PSBoundParameters.ContainsKey("PostBootDebugScripts"))
        {
            if (-not $Request.ContainsKey("debugScripts"))
            {
                $Request.Add("debugScripts", @{})
            }

            $Request["debugScripts"].Add("postboot", $PostBootDebugScripts)
        }

        if ($PSBoundParameters.ContainsKey("PreBootDebugScripts"))
        {
            if (-not $Request.ContainsKey("debugScripts"))
            {
                $Request.Add("debugScripts", @{})
            }

            $Request["debugScripts"].Add("preboot", $PreBootDebugScripts)
        }

        [System.String]$Body = ConvertTo-Json -InputObject $Request

        [System.String]$UrlPath = "/projects/$ProjectId/launchMachines"

        $ConfirmMessage = @"
This $LaunchType will launch a new instance for each of the launchable Source instances that you have selected.
 
In addition, the Source instance will be marked as $(switch ($LaunchType) { "CUTOVER" { "cutover"; break; } "TEST" { "tested"; break; } "RECOVERY" {"recovered"; break;} "DEBUG" {"debugged"; break;} }) on this date.
 
Note:
Any previously launched versions of these instances (including any associated cloud resources that were created by CloudEndure) will be deleted.
"@

        $WhatIfDescription = "$LaunchType $($Items.Length) instance$(if ($Items.Length -gt 1) { "s" })."
        $ConfirmCaption = "$LaunchType $($Items.Length) Instance$(if ($Items.Length -gt 1) { "s" })"

        if ($Force -or $PSCmdlet.ShouldProcess($WhatIfDescription, $ConfirmMessage, $ConfirmCaption))
        {
            Write-Verbose -Message "Requesting $LaunchType for:`r`n $Body"

            try {
                $ErrorHandling = {
                    Param($StatusCode, $Content, $ErrorMessage)
        
                    switch ($StatusCode)
                    {
                        202 {
                            Write-Output -InputObject $Content
                            break
                        }
                        400 {
                            throw "Another job is alreading running in this project"
                        }    
                        402 {
                            throw "Project license has expired."
                        }
                        default {
                            throw $ErrorMessage
                        }            
                    }
                }

                $Result = Invoke-CERequest -Path $UrlPath -Method Post -Body $Body -Session $Session -ErrorHandling $ErrorHandling
            }
            catch [Exception] {
                throw "There was an issue launching target machines: $($_.Exception.Message)"
            }
        }
    }

    End {

    }
}

Function Move-CEMachine {
    <#
        .SYNOPSIS
            Moves machines to another project
 
        .DESCRIPTION
            This cmdlet moves CE machines from one project to another.
 
        .PARAMETER Ids
            The Ids of the CE machines to move.
 
        .PARAMETER ProjectId
            The project Id that the machines are part of. This defaults to the current project retrieved from the login.
 
        .PARAMETER DestinationProjectId
            The project Id that the machines will be moved to.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .EXAMPLE
            Move-CEMachine -Ids @("f1ffcc9b-8988-4273-acba-96828ef24b0e") -DestinationProjectId d33213b9-cf05-4a19-9b3c-45605a14eaea
 
            Moves the specified instances from the default current project to the specified destination project.
 
        .INPUTS
            System.Guid[]
 
        .OUTPUTS
            None
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 12/18/2019
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "HIGH")]
    [OutputType([System.Management.Automation.PSCustomObject])]
    Param(
        [Parameter(Mandatory = $true, Position = 1)]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$DestinationProjectId,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [System.Guid[]]$Ids,

        [Parameter(Position = 0, ValueFromPipeline = $true)]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [Switch]$Force,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.ProjectId
        }

        [System.Collections.Hashtable]$Request = @{ "destinationProjectId" = $DestinationProjectId; "machineIDs" = $Ids}
        [System.String]$Body = ConvertTo-Json -InputObject $Request

        [System.String]$UrlPath = "/projects/$ProjectId/moveMacines"

        $ConfirmMessage = @"
This will move $($Ids.Length) instance$(if ($Ids.Length -gt 1) { "s" }) from project $ProjectId to $DestinationProjectId.
"@

        $WhatIfDescription = "Moved $($Ids.Length) instance$(if ($Items.Length -gt 1) { "s" })."
        $ConfirmCaption = "Move $($Ids.Length) Instance$(if ($Items.Length -gt 1) { "s" })"

        if ($Force -or $PSCmdlet.ShouldProcess($WhatIfDescription, $ConfirmMessage, $ConfirmCaption))
        {
            Write-Verbose -Message "Moving machines:`r`n $Body"

            try {
                $ErrorHandling = {
                    Param($StatusCode, $Content, $ErrorMessage)
        
                    switch ($StatusCode)
                    {
                        204 {
                            Write-Output -InputObject $Content
                            break
                        }
                        404 {
                            throw "A machine or project not found in account = $Content."
                        }    
                        409 {
                            throw "Machines could not be moved due to a conflict = $Content."
                        }
                        default {
                            throw $ErrorMessage
                        }            
                    }
                }

                $Result = Invoke-CERequest -Path $UrlPath -Method Post -Body $Body -Session $Session -ErrorHandling $ErrorHandling
            }
            catch [Exception] {
                throw "There was an issue moving the machines: $($_.Exception.Message)"
            }
        }
    }

    End {

    }
}

#endregion

#region Configuration

Function Set-CEUserPassword {
    <#
        .SYNOPSIS
            Sets the password for an invited user.
 
        .DESCRIPTION
            The cmdlet sets the password for an invited user.
 
        .PARAMETER Token
            The token of the user to set.
 
        .PARAMETER NewPassword
            The new password for the account. It must 8 characters or more, 1 upper, 1 lower, 1 numeric, and 1 special character.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .EXAMPLE
            Set-CEUserPassword -Token $Token -NewPassword @$3cureP@$$w0rd
             
            The cmdlet sets the password.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 12/18/2019
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "HIGH")]
    [OutputType()]
    Param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Token,

        [Parameter(Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [ValidatePattern("^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$@$!%*?&])[A-Za-z\d$@$!%*?&]{8,}")]
        [System.String]$NewPassword,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }

    Process {
        [System.String]$UrlPath = "/setPassword"

        [System.String]$Body = @{
            "token" = $Token;
            "newPassword" = $NewPassword
        } | ConvertTo-Json

        $ConfirmMessage = "Are you sure you want to set the user's password?"
        $WhatIfDescription = "Updated password for $Token."
        $ConfirmCaption = "Update Password for $Token."

        if ($Force -or $PSCmdlet.ShouldProcess($WhatIfDescription, $ConfirmMessage, $ConfirmCaption))
        {
            Write-Verbose -Message "Sending updated config:`r`n$Body"

            $ErrorHandling = {
                Param($StatusCode, $Content, $ErrorMessage)
                
                switch ($StatusCode)
                {
                    204 {
                        Write-Verbose -Message "Password successfully updated."
                        break
                    }
                    400 {
                        throw "Password change did not succeed (e.g. Old password mismatch).`r`n$Content"
                    }
                    401 {
                        throw "Invalid set password token.`r`n$Content"
                    }
                    default {
                        throw "There was an issue with changing the password: $ErrorMessage"
                    }
                }
            }

            try {
                $Result = Invoke-CERequest -Path $Path -Method Post -Body $Body -Session $Session -ErrorHandling $ErrorHandling
            }
            catch [Exception] {
                throw "There was an issue setting the password: $($_.Exception.Message)"
            }
        }
    }

    End {
    }
}

Function Get-CEProjectStorage {
    <#
        .SYNOPSIS
            Gets the project's storage usage (vCenter only).
 
        .DESCRIPTION
            The cmdlet gets the project's storage usage (vCenter only).
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
             
        .PARAMETER ProjectId
            The project Id to use to retrieve the details. This defaults to the current project retrieved from the login.
 
        .EXAMPLE
            $Storage = Get-CEProjectStorage
 
            Gets the storage data for the default project.
 
        .INPUTS
            None
 
        .OUTPUTS
           System.Management.Automation.PSCustomObject
 
            This is a json representation of the output:
            {
              "projectId": "string",
              "pointsInTimeTotalBytes": 0,
              "runningMachinesTotalBytes": 0,
              "workingStorage": {
                "disksTotalBytes": 0,
                "snapshotsTotalBytes": 0
              },
              "calculationDateTime": "2019-12-18T18:59:29Z"
            }
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 12/18/2019
    #>

    [CmdletBinding()]
    [OutputType([System.String])]
    Param(
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {        
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session
        
        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.DefaultProjectId
        }

        [System.String]$UrlPath = "/projects/$ProjectId/storage"

        try {
            $Result = Invoke-CERequest -Path $UrlPath -Method Get -Session $Session -ExpectedResponseCode 200
            Write-Output -InputObject $Result
        }
        catch [Exception] {
            throw "There was an issue retrieving the project storage details: $($_.Exception.Message)"
        }
    }

    End {
    }
}

Function Add-CEUserRoles {
    <#
        .SYNOPSIS
            Adds roles to a user.
 
        .DESCRIPTION
            The cmdlet adds roles to a user.
 
        .PARAMETER UserId
            The id of the user that will have roles added to.
 
        .PARAMETER Roles
            The roles to add, either USER, ACCOUNT_ADMIN, or GLOBAL_READONLY.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .EXAMPLE
            Add-CEUserRoles -UserId $Id -Roles @("USER")
             
            The cmdlet add the role USER to the user.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/24/2020
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "HIGH")]
    [OutputType()]
    Param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]$UserId,

        [Parameter(Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true)]
        [ValidateSet("USER", "ACCOUNT_ADMIN", "GLOBAL_READONLY")]
        [System.String[]]$Roles,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }

    Process {
        [System.String]$UrlPath = "/users/assignRoles"

        [System.String]$Body = @{
            "items" = @(
                @{
                    "userId" = $UserId;
                    "roles" = $Roles
                }
            )                    
        } | ConvertTo-Json

        $ConfirmMessage = "Are you sure you want to add roles to the user?"
        $WhatIfDescription = "Updated roles for $UserId."
        $ConfirmCaption = "Add $([System.String]::Join(", ", $Roles)) to $UserId."

        if ($Force -or $PSCmdlet.ShouldProcess($WhatIfDescription, $ConfirmMessage, $ConfirmCaption))
        {
            Write-Verbose -Message "Sending role update config:`r`n$Body"

            $ErrorHandling = {
                Param($StatusCode, $Content, $ErrorMessage)
                
                switch ($StatusCode)
                {
                    204 {
                        Write-Verbose -Message "Password successfully updated."
                        break
                    }
                    404 {
                        throw "The specified user does not exist."
                    }                    
                    default {
                        throw $ErrorMessage
                    }
                }
            }

            try {
                $Result = Invoke-CERequest -Path $Path -Method Post -Body $Body -Session $Session -ErrorHandling $ErrorHandling
            }
            catch [Exception] {
                throw "There was an issue adding the roles: $($_.Exception.Message)"
            }
        }
    }

    End {
    }
}

Function Add-CEUserToProject {
    <#
        .SYNOPSIS
            Assigns users to a project.
 
        .DESCRIPTION
            The cmdlet adds one or more users to a project.
 
        .PARAMETER UserIds
            The ids of the users to add to the project. Each Id is a UUID.
 
        .PARAMETER ProjectId
            The id of the project to add the users to, which is a UUID.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .EXAMPLE
            Add-CEUserToProject -UserIds $Id -ProjectId $ProjectId
             
            The cmdlet adds the user Ids to the project.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/24/2020
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "HIGH")]
    [OutputType()]
    Param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.Guid[]]$UserIds,

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.ProjectId
        }

        [System.String]$UrlPath = "/projects/assignUsers"

        [System.String]$Body = ConvertTo-Json -InputObject @{"items" = @(@{"projectId" = $ProjectId; "userIDs" = $UserIds})}

        try {
            $ErrorHandling = {
                Param($StatusCode, $Content, $ErrorMessage)
            
                switch ($StatusCode)
                {
                    200 {
                        Write-Output -InputObject $Content
                        break
                    }
                    404 {
                        throw "Either the project or one or more of the users do not exist."
                    }                    
                    default {
                        throw $ErrorMessage
                    }            
                }
            }

            $Result = Invoke-CERequest -Path $UrlPath -Method Post -Body $Body -Session $Session -ErrorHandling $ErrorHandling

        }
        catch [Exception] {
            throw "There was an issue adding the users to the project: $($_.Exception.Message)"
        }
    }

    End {
    }
}

Function Remove-CEUserFromProject {
    <#
        .SYNOPSIS
            Removes users from a project.
 
        .DESCRIPTION
            The cmdlet removes one or more users from a project.
 
        .PARAMETER UserIds
            The ids of the users to remove from the project. Each Id is a UUID.
 
        .PARAMETER ProjectId
            The id of the project to remove the users from, which is a UUID.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .EXAMPLE
            Remove-CEUserToProject -UserIds $Id -ProjectId $ProjectId
             
            The cmdlet removes the user Ids from the project.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/24/2020
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "HIGH")]
    [OutputType()]
    Param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.Guid[]]$UserIds,

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty,

        [Parameter()]
        [Switch]$Force
    )

    Begin {
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.ProjectId
        }

        [System.String]$UrlPath = "/projects/removeUsers"

        [System.String]$Body = ConvertTo-Json -InputObject @{"items" = @(@{"projectId" = $ProjectId; "userIDs" = $UserIds})}

        $ConfirmMessage = "Are you sure you want to remove the users $([System.String]::Join(", ", $UserIds)) from the project $ProjectId?"
        $WhatIfDescription = "Removed users from the project."
        $ConfirmCaption = "Removed users from the project."

        if ($Force -or $PSCmdlet.ShouldProcess($WhatIfDescription, $ConfirmMessage, $ConfirmCaption))
        {
            try {
                $Result = Invoke-CERequest -Path $UrlPath -Method Post -Body $Body -Session $Session -ExpectedResponseCode 200
            }
            catch [Exception] {
                throw "There was an issue adding the users to the project: $($_.Exception.Message)"
            }
        }
    }

    End {
    }
}

Function Get-CEAuditLog {
    <#
        .SYNOPSIS
            Gets the audit log for a project.
 
        .DESCRIPTION
            The cmdlet gets audit log entries for a specified project. There may be more items beyond the limit, but they cannot be retrieved via this cmdlet because the API does not provide an offset parameter.
 
        .PARAMETER ProjectId
            The id of the project to get the audit log for, which is a UUID.
 
        .PARAMETER Limit
            A number specifying how many entries to return from 1 to 1500. Defaults to 1500.
 
        .PARAMETER FromDateTime
            Used to limit the response to a specific date range. Must be used in conjunction with the ToDateTime parameter.
 
        .PARAMETER ToDateTime
            Used to limit the response to a specific date range. Must be used in conjunction with the FromDateTime parameter.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
 
        .EXAMPLE
            $LogEntries = Get-CEAuditLog -ProjectId $ProjectId
             
            The cmdlet adds the user Ids to the project.
 
        .INPUTS
            None
 
        .OUTPUTS
            System.Management.Automation.PSCustomObject[]
 
             [
                {
                  "username": "string",
                  "eventName": "string",
                  "participatingMachines": [
                    {
                      "machineCloudEndureId": "string",
                      "machineCloudName": "string",
                      "machineCloudId": "string"
                    }
                  ],
                  "description": "string",
                  "changedFields": [
                    {
                      "fieldName": "string",
                      "newValue": "string",
                      "oldValue": "string"
                    }
                  ],
                  "timestamp": "2020-01-24T19:07:56Z",
                  "jobId": "string"
                }
              ]
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/24/2020
    #>

    [CmdletBinding(DefaultParameterSetName = "__AllParameterSets")]
    [OutputType([PSCustomObject[]])]
    Param(
        [Parameter()]
        [ValidateRange(1, 1500)]
        [System.Int32]$Limit = 1500,

        [Parameter(ParameterSetName = "Date", Mandatory = $true)]
        [ValidateNotNull()]
        [System.DateTime]$FromDateTime,

        [Parameter(ParameterSetName = "Date", Mandatory = $true)]
        [ValidateNotNull()]
        [System.DateTime]$ToDateTime,

        [Parameter()]
        [ValidateSet("json")]
        [System.String]$Format = [System.String]::Empty,

        [Parameter()]
        [ValidateScript({
            $_ -ne [System.Guid]::Empty
        })]
        [System.Guid]$ProjectId = [System.Guid]::Empty,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {
    }

    Process {
        $SessionInfo = Get-CESessionOrDefault -Session $Session

        if ($ProjectId -eq [System.Guid]::Empty)
        {
            $ProjectId = $SessionInfo.ProjectId
        }

        [System.String]$UrlPath = "/projects/$ProjectId/auditLog"

        try {
            $QueryString = [System.String]::Empty

            if ($Limit -lt 1500)
            {
                $QueryString += "&limit=$Limit"
            }

            if ($PSCmdlet.ParameterSetName -eq "Date")
            {
                $QueryString += "&fromDatetime=$($FromDateTime.ToString("yyyyMMdd"))"
                $QueryString += "&toDatetime=$($FromDateTime.ToString("yyyyMMdd"))"
            }

            if (-not [System.String]::IsNullOrEmpty($Format))
            {
                $QueryString += "&format=$Format"
            }

            if (-not [System.String]::IsNullOrEmpty($QueryString))
            {
                # Remove the first character which is an unecessary ampersand
                $UrlPath += "?$($QueryString.Substring(1))"
            }

            $Result = Invoke-CERequest -Method Get -Path $UrlPath -Session $Session -ExpectedResponseCode 200
            $Results += ($Result | Select-Object -ExpandProperty items)
            Write-Output -InputObject $Results
                
        }
        catch [Exception] {
            throw "There was an issue retrieving the audit log: $($_.Exception.Message)"
        }
    }

    End {
    }
}

Function New-CEUser {
    <#
        .SYNOPSIS
            Creates a new CloudEndure user.
 
        .DESCRIPTION
            The cmdlet creates a new CE User.
 
        .PARAMETER UserName
            The name of the new user.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
             
 
        .EXAMPLE
            $User = New-CEUser -UserName "john.smith@cloudendure.com" -PassThru
 
            Creates a new user named john.smith@cloudendure.com.
 
        .INPUTS
            System.String
 
        .OUTPUTS
           System.Management.Automation.PSCustomObject
 
            This is a json representation of the output:
            {
              "username": "user@example.com",
              "status": "PENDING",
              "account": "string",
              "roles": [
                "USER"
              ],
              "settings": {
                "sendNotifications": {
                  "projectIDs": [
                    "string"
                  ],
                  "projectIDsUntestedMigrations": [
                    "string"
                  ]
                }
              },
              "apiToken": "string",
              "hasPassword": true,
              "termsAccepted": true,
              "id": "string",
              "selfLink": "string"
            }
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/24/2020
    #>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSCustomObject])]
    Param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]$UserName,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty,

        [Parameter()]
        [Switch]$PassThru
    )

    Begin {        
    }

    Process {
        [System.String]$UrlPath = "/users"

        [System.String]$Body = ConvertFrom-Json -InputObject @{"username" = $UserName}

        try {
            $Result = Invoke-CERequest -Path $UrlPath -Method Post -Body $Body -Session $Session -ExpectedResponseCode 200
            
            if ($PassThru)
            {
                Write-Output -InputObject $Result
            }
        }
        catch [Exception] {
            throw "There was an issue creating the new user: $($_.Exception.Message)"
        }
    }

    End {
    }
}

Function New-CEApiToken {
    <#
        .SYNOPSIS
            Replaces the current API token.
 
        .DESCRIPTION
            The cmdlet replaces the current API token.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
             
 
        .EXAMPLE
            $Token = New-CEApiToken
 
            Replaces the existing API token.
 
        .INPUTS
            None
 
        .OUTPUTS
           System.String
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/24/2020
    #>

    [CmdletBinding()]
    [OutputType([System.String])]
    Param(
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {        
    }

    Process {
        [System.String]$UrlPath = "/replaceApiToken"

        try {
            $Result = Invoke-CERequest -Path $UrlPath -Method Post -Body ([System.String]::Empty) -Session $Session -ExpectedResponseCode 200            
            Write-Output -InputObject ($Result | Select-Object -ExpandProperty apiToken)            
        }
        catch [Exception] {
            throw "There was an issue replacing the API token: $($_.Exception.Message)"
        }
    }

    End {
    }
}

Function Get-CETemporaryToken {
    <#
        .SYNOPSIS
            Gets a temporary token by email. Available for account owner when SSO is used.
 
        .DESCRIPTION
            Gets a temporary token by email. Available for account owner when SSO is used.
 
        .PARAMETER AccountId
            The account id for the user.
 
        .PARAMETER Session
            The session identifier provided by New-CESession. If this is not specified, the default session information will be used.
             
        .PARAMETER UserName
            The user name to get the temporary token for, it is an email address.
 
        .EXAMPLE
            $Message = Get-CETemporaryToken
 
            Sends a temporary, one-time, time-limited token by email if conditions were met.
 
        .INPUTS
            None
 
        .OUTPUTS
           System.String
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 1/24/2020
    #>

    [CmdletBinding()]
    [OutputType([System.String])]
    Param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]$UserName,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.Guid]$AccountId,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            $script:Sessions.ContainsKey($_.ToLower())
        })]
        [System.String]$Session = [System.String]::Empty
    )

    Begin {        
    }

    Process {
        [System.String]$UrlPath = "/accounts/$AccoundId/access?username=$UserName"

        try {
            $Result = Invoke-CERequest -Path $UrlPath -Method Get -Session $Session -ExpectedResponseCode 200            
            Write-Output -InputObject ($Result | Select-Object -ExpandProperty message)            
        }
        catch [Exception] {
            throw "There was an issue creating the temporary token: $($_.Exception.Message)"
        }
    }

    End {
    }
}

#endregion

#region Misc

Function Get-CEWindowsInstaller {
    <#
        .SYNOPSIS
            Downloads the CloudEndure windows installer.
 
        .DESCRIPTION
            The cmdlet downloads the installer to a location you specify.
 
        .PARAMETER Destination
            The location the installer should be downloaded to. This can be either a folder or a file name, such as
 
            c:\downloads or c:\downloads\installer.exe. If a filename is not specified, the filename of the file will be used.
             
        .EXAMPLE
            Get-CEWindowsInstaller -Destination c:\
 
            Downloads the windows installer to the c: drive.
 
        .INPUTS
            System.String
 
        .OUTPUTS
           None
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 12/18/2019
    #>

    [CmdletBinding()]
    [OutputType()]
    Param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Destination
    )

    Begin {        
    }

    Process {

        if (Test-Path -Path $Destination -PathType Container)
        {
            $Destination = Join-Path -Path $Destination -ChildPath "installer_win.exe"
        }
        else {
            # The regex is starts with c: or \ and then one or more \dir pieces and then an extension with .ab with 2 or more characters
            # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx Defines the non-allowed file path characters
            if ($Destination -imatch "^(?:[a-zA-Z]:|\\)(?:\\[^<>:`"\/\\|\?\*]+)+\.[^<>:`"\/\\|\?\*]{2,}$")
            {
                # Then the provided path is a file name, make sure the directory exists
                
                [System.IO.FileInfo]$FileInfo = New-Object -TypeName System.IO.FileInfo($Destination)
                if (-not (Test-Path -Path $FileInfo.Directory.FullName))
                {
                    New-Item -Path $FileInfo.Directory.FullName -ItemType Directory
                }
            }
            else
            {
                # Treat the path as a directory, make sure it exists
                if (-not (Test-Path -Path $Destination))
                {
                    New-Item -Path $Destination -ItemType Directory
                }

                $Destination = Join-Path -Path $Destination -ChildPath "installer_win.exe"
            }
        }

        $StatusCode = 0
        $Reason = ""

        try {
            # Now we know the folder for the destination exists and the destination path includes a file name
            # OutFile writes the file to the provided path
            # PassThru is only an option with OutFile and returns the result to the pipeline
            [Microsoft.PowerShell.Commands.WebResponseObject]$Result = Invoke-WebRequest -Uri $script:Installer -OutFile $Destination -PassThru -ErrorAction Stop
            $StatusCode = $Result.StatusCode
            $Reason = $Result.StatusDescription
        }
        catch [System.Net.WebException] {
            [System.Net.WebException]$Ex = $_.Exception

            if ($Ex.Response -eq $null)
            {
                $Reason = "$($Ex.Status): $($Ex.Message)"
                $StatusCode = 500
            }
            else
            {
                [System.Net.HttpWebResponse]$Response = $Ex.Response
                $StatusCode = [System.Int32]$Response.StatusCode
            
                [System.IO.Stream]$Stream = $Response.GetResponseStream()
                [System.Text.Encoding]$Encoding = [System.Text.Encoding]::GetEncoding("utf-8")
                [System.IO.StreamReader]$Reader = New-Object -TypeName System.IO.StreamReader($Stream, $Encoding)
                $Content = $Reader.ReadToEnd()

                $Reason = "$($Response.StatusDescription) $($_.Exception.Message)`r`n$Content"
            }
        }
        catch [Exception]  {
            $Reason = $_.Exception.Message
        }

        if ($StatusCode -ne 200) {
            throw "There was an issue downloading this file to $Destination`: $StatusCode $Reason - $($Result.Content)"
        }
        else {
            Write-Verbose -Message "Download compeleted successfully."
        }
    }

    End {
    }
}

#endregion