PowerVCAV.psm1

# PowerVCAV.psm1
#
# PS Module to more easily interrogate the vCloud Availability 3.0 API
#
# Requires that you are already connected to the appropriate vCloud Director
# site(s) - powervcav will use the $Global:DefaultCIServers context to extract
# vCD session keys and use these to authenticate to vCAv. VMware PowerCLI is
# required for this module to function.
#
# Copyright 2019 Jon Waite, All Rights Reserved
# Released under MIT License - see https://opensource.org/licenses/MIT
# Date: 9th September 2019
# Version: 0.2.3
#

Function Connect-VCAV {
<#
.SYNOPSIS
Connect-VCAV makes a connection to the vCloud Availability (VCAV) 3.0 API
.DESCRIPTION
Connect-VCAV uses the session secret of an existing vCloud Director
connection (obtained from Connect-CIServer). If more that one vCloud Director
connection exists both the VCAV host and VCD host must be specified. The VCAV
session is persisted in session variables for use by other commands in this
module (e.g. Invoke-VCAVQuery).
.PARAMETER VCAVHost
A required parameter containing the API endpoint for the vCloud Availability
service, typically this will be the public URI for the VCAV service.
.PARAMETER VCDHost
The API endpoint for vCloud Director, typically this will be the public URI for
the vCloud Director instance. This parameter is required if multiple vCD API
endpoints are currently connected, otherwise it is optional.
.OUTPUTS
A status message (success or failure) is given for the connection attempt.
.EXAMPLE
Connect-VCAV -VCAVHost 'myvcav.cloud.com' -VCDHost 'myvcd.cloud.com'
.NOTES
If more than one vCD environment is currently connected (Connect-CIServer), the
host name specified for the VCDHost must match the name used when the original
Connect-CIServer command was issued so that the correct session can be located
in the $Global:DefaultCIServers array.
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][string]$VCAVHost,
        [Parameter()][string]$VCDHost
    )
     
    # Check if we are already connected to this VCAVHost:
    if ($Script:VCAVIsConnected -eq $true) {
        Write-Error ("Already connected to VCAV, logout before attempting to login again")
        return
    }

    $vCDCount = $Global:DefaultCIServers.Count

    # Not connected to any vCD instances, can't continue:
    if ($vCDCount -eq 0) {
        Write-Error ("Not currently connected to any vCloud Director cells, cannot attach to vCloud Availability - Use Connect-CIServer first")
        return
    }

    if ($vCDCount -gt 1) {
        # We are connected to multiple vCD environments, match the supplied $VCDHost in the global array:
        $vCDSecret = ($Global:DefaultCIServers | Where-Object { $_.Name -eq $VCDHost }).SessionSecret
        if (!$vCDSecret) {
            Write-Error ("Cannot find a connection to vCloud Director that matches host $VCDHost, connect first using Connect-CIServer with this hostname")
            return
        } 
    }
    else {
        # There is only 1 connection so just use the session secret from that:
        $vCDSecret = $Global:DefaultCIServers.SessionSecret
    }

    # Attempt to establish initial/primary connection to the vCAV API:
    $AuthBody = [PSCustomObject]@{
        type      = "vcdCookie"
        vcdCookie = $vcdSecret
    } | ConvertTo-Json -Compress

    try {
        $PriVCAV = Invoke-WebRequest -Uri "https://$VCAVHost/sessions" -Method Post -ContentType 'application/json' -Body $AuthBody
    }
    catch {
        Write-Error ("Could not connect to vCloud Availability, error message: " + $_.Exception.Message)
        $Script:VCAVIsConnected = $false
        Break
    }
    
    $Script:VCAVHost = $VCAVHost
    $Script:VCAVIsConnected = $true
    if ($PriVCAV.Headers.'X-VCAV-Auth' -is [Array]) {
        $Script:VCAVToken = $PriVCAV.Headers.'X-VCAV-Auth'[0]
    } else {
        $Script:VCAVToken = $PriVCAV.Headers.'X-VCAV-Auth'
    }
    $Script:VCDPriHost = $VCDHost
    Write-Host -ForegroundColor Green ("Logged in to VCAV successfully")
    return
}

Function Disconnect-VCAV {
<#
.SYNOPSIS
Disconnect-VCAV logs out from the vCloud Availability (VCAV) 3.0 API
.DESCRIPTION
Disconnect-VCAV clears any sessions from the vCloud Availability API including
any extended sites
.OUTPUTS
A status message (success or failure) is given for the disconnection attempt.
.EXAMPLE
Disconnect-VCAV
.NOTES
If a vCloud Availability API session has timed-out due to inactivity the
Disconnect-VCAV command will clear out the stale session variables so that
Connect-VCAV can be re-used to establish a new session.
#>

    [CmdletBinding()]
    param ()

    if (!$Script:VCAVHost) {
        Write-Error ("Can't find session, exiting.")
        exit
    }
    $Uri = "https://$Script:VCAVHost/sessions"
    $VCAHeader = @{'X-VCAV-Auth' = $($VCAVToken) }
    
    # Clear session state variables:
    if ($Script:VCAVHost) { Remove-Variable -Scope Script -Name VCAVHost -Confirm:$false }
    if ($Script:VCAVToken) { Remove-Variable -Scope Script -Name VCAVToken -Confirm:$false }
    if ($Script:VCAVIsConnected) { Remove-Variable -Scope Script -Name VCAVIsConnected -Confirm:$false }
    if ($Script:VCDPriHost) { Remove-Variable -Scope Script -Name VCDPriHost -Confirm:$false }
    
    Try {
        Invoke-WebRequest -Uri $Uri -Method Delete -Headers $VCAHeader -ErrorAction Stop | Out-Null
    }
    Catch {
        Write-Error ("ERROR: " + $_.Exception.Message)
        Break
    }
    Write-Host -ForegroundColor Green "Logged out successfully."
}

Function Connect-VCAVExtend {
<#
.SYNOPSIS
Connect-VCAVExtend extends the current vCloud Availability session to
additional VCAV sites.
.DESCRIPTION
Connect-VCAVExtend uses the session secret of an existing vCloud Director
connections (obtained from Connect-CIServer) to extend the VCAV session to
additional sites. The VCAV Site Name and VCD host to be extended to must
be specified. The extended session is persisted in session variables for use
by other commands in this module (e.g. Invoke-VCAVQuery).
.PARAMETER VCAVHost
A required parameter containing the VCAV Site Name (e.g. 'My-2nd-Site') which
matches the vCloud Director endpoint specified in -VCDHost.
.PARAMETER VCDHost
A required parameter containing The API endpoint for vCloud Director at the
site to be extended to, typically this will be the public URI for the vCloud
Director instance.
.OUTPUTS
A status message (success or failure) is given for the connection extension
attempt.
.EXAMPLE
Connect-VCAVExtend -VCAVSiteName 'Site2' -VCDHost 'vcdsite2.cloud.com'
.NOTES
The host name specified for the VCDHost must match the name used when the
original Connect-CIServer command was issued so that the correct session can be
located in the $Global:DefaultCIServers array. A list of valid VCAV site names
can be obtained using the Invoke-VCAVQuery cmdlet with -QueryPath of 'sites'.
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][string]$VCAVSiteName,
        [Parameter(Mandatory = $true)][string]$VCDHost
    )
    if ($Script:VCAVIsConnected -ne $true) # Not authenticated to API
    { Write-Error ("VCAV not logged in, cannot extend to another site, use Connect-VCAV first."); return }

    $vCDSecret = ($Global:DefaultCIServers | Where-Object { $_.Name -eq $VCDHost }).SessionSecret
    if (!$vCDSecret) {
        Write-Error ("Cannot find a connection to vCloud Director that matches host $VCDHost, connect first using Connect-CIServer with this hostname.")
        return
    } 

    $AuthBody = [PSCustomObject]@{
        type   = "cookie"
        site   = $VCAVSiteName
        cookie = $vCDSecret
    } | ConvertTo-Json -Compress

    $VCAVHeader = @{'X-VCAV-Auth' = $Script:VCAVToken }

    Try {
        Invoke-WebRequest -Uri "https://$Script:VCAVHost/sessions/extend" -Method Post -Headers $VCAVHeader -Body $AuthBody -ContentType 'application/json' -ErrorAction Stop | Out-Null
    }
    Catch {
        Write-Error ("ERROR: " + $_.Exception.Message)
        Break
    }
    Write-Host -ForegroundColor Green ("Extended session to " + $VCAVSite + " successfully.")
    return
}

Function Get-VCAVToken {
<#
.SYNOPSIS
Returns the vCloud Availability session token for the current session.
.DESCRIPTION
Get-VCAVToken returns a string containing the VCAV session token for the
currently connected VCAV session as used in the 'X-VCAV-Auth' token in
API requests.
.OUTPUTS
A string containing the VCAV token for the current VCAV session.
.EXAMPLE
Get-VCAVToken
.NOTES
If no session is currently connected an empty string will be returned.
#>

    [CmdletBinding()]
    param()
    return $Script:VCAVToken
}

Function Invoke-VCAVPagedQuery {
<#
.SYNOPSIS
Query the vCloud Availability (VCAV) API
.DESCRIPTION
Invoke-VCAVPagedQuery queries the vCloud Availability API for the specified
resource which is returned as a PSCustomObject. Queries which return more than
100 objects are split into 100 object chunks so that all returned values are
obtained. Parameters can be specified to limit the returned results by a filter
(see Examples).
.PARAMETER QueryPath
A required parameter containing the API resource to retrieve.
.PARAMETER Headers
An optional parameter containing any additional HTML headers to pass to the API
note that the 'X-VCAV-Auth' token is populated automatically based on existing
sessions to the API and the 'Accept' token is automatically set to the value
'application/vnd.vmware.h4-v3+json;charset=UTF-8' if not specified. This is
appropriate for the majority of VCAV API queries.
.PARAMETER Filter
An optional parameter to restrict the results returned by encoding additional
filters in the query Uri. See https://code.vmware.com/apis/441/vcav for
details of the valid filter parameters for each method call.
.OUTPUTS
A PSCustomObject containing the resources from the API call or an error.
.EXAMPLE
Get a list of VCAV vApp Replications:
Invoke-VCAVPagedQuery -QueryPath 'vapp-replications'
.EXAMPLE
Retrieve a list of vApp replications for the vCD Organization 'myorg':
Invoke-VCAVPagedQuery -QueryPath 'vapp-replications' -Filter @{ sourceSite='myorg' }
.NOTES
The parameters used in the -Filter argument for each query type are specified
in the VCAV API documentation at https://code.vmware.com/apis/441/vcav
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][string]$QueryPath,
        [Parameter()][hashtable]$Headers,
        [Parameter()][hashtable]$Filter
    )
    $offset = 0
    $limit = 100
    $items = @()
    if (!$Filter) { $Filter = @{} }
    While ($true) {
        $Uri = New-VCAVUrl -QueryPath $QueryPath -Filter (@{offset = $offset; limit = $limit } + $Filter)
        $result = Invoke-VCAVQuery -Uri $Uri -Method Get -Headers $Headers
        $items += ($result.items)
        $offset += $result.items.Count
        if ($offset -ge $result.total) { Break }
    }
    return $items
}

Function Invoke-VCAVQuery {
<#
.SYNOPSIS
Query the vCloud Availability (VCAV) API
.DESCRIPTION
Invoke-VCAVQuery queries the vCloud Availability API for the specified resource
which is returned as a PSCustomObject. Parameters can be specified to limit
the returned results by a filter (see Examples).
.PARAMETER Uri
An optional parameter containing the absolute URI to be queried, note that if
this parameter is used the -QueryPath and -Filter parameters are ignored.
.PARAMETER QueryPath
An optional parameter containing the API resource to retrieve.
.PARAMETER Method
An optional parameter containing the HTML method to use in the query
(default='Get' if not specified)
.PARAMETER Headers
An optional parameter containing any additional HTML headers to pass to the API
note that the 'X-VCAV-Auth' token is populated automatically based on existing
sessions to the API and the 'Accept' token is automatically set to the value
'application/vnd.vmware.h4-v3+json;charset=UTF-8' if not specified. This is
appropriate for the majority of VCAV API queries.
.PARAMETER Filter
An optional parameter to restrict the results returned by encoding additional
filters in the query Uri. See https://code.vmware.com/apis/441/vcav for
details of the valid filter parameters for each method call.
.PARAMETER ContentType
An optional parameter specifying the HTML ContentType of the submitted API
request, generally this should only be specified for requests which send data
to the API using the -Body parameter.
.PARAMETER Body
An optional parameter specifying the JSON document to be submitted to the VCAV
API, generally this will also require setting the -ContentType parameter.
.OUTPUTS
A PSCustomObject containing the resources from the API call or an error.
.EXAMPLE
Get a list of VCAV organizations:
Invoke-VCAVQuery -QueryPath 'inventory/orgs'
.EXAMPLE
Retrieve a list of VCAV sites:
Invoke-VCAVQuery -QueryPath 'sites'
.EXAMPLE
Retrieve the current connection details for this VCAV session:
Invoke-VCAVQuery -QueryPath 'sessions'
.NOTES
For queries which can return a large number of results (>100 typically) use
the Invoke-VCAVPagedQuery cmdlet to ensure that all results are retrieved.
#>

    [CmdletBinding()]
    param(
        [Parameter()][string]$Uri,
        [Parameter()][string]$QueryPath,
        [Parameter()][Microsoft.PowerShell.Commands.WebRequestMethod]$Method = 'Get',
        [Parameter()][hashtable]$Headers,
        [Parameter()][hashtable]$Filter,
        [Parameter()][string]$ContentType,
        [Parameter()][string]$Body
    )

    if ($Script:VCAVIsConnected -ne $true) # Not authenticated to API
    { Write-Error ("Not connected to VCAV API, authenticate first with Connect-VCAV"); Break }
    
    if (!$Uri) {
        $UriParams = @{ QueryPath = $QueryPath }
        if ($Filter) { $UriParams.Filter = $Filter }
        $Uri = New-VCAVUrl @UriParams
    }

    if (!$Headers) { $Headers = @{ } 
    }
    
    if (! ($Headers.ContainsKey('X-VCAV-Auth'))) {
        $Headers.Add('X-VCAV-Auth', $Script:VCAVToken)
    }

    if (! ($Headers.ContainsKey('Accept'))) {
        $Headers.Add('Accept', 'application/vnd.vmware.h4-v3+json;charset=UTF-8')
    }

    $InvokeParams = @{
        Uri     = $Uri
        Method  = $Method
        Headers = $Headers
    }
    if ($ContentType) { $InvokeParams.ContentType = $ContentType }
    if ($Body) { $InvokeParams.Body = $Body }
    Try {
        $result = Invoke-RestMethod @InvokeParams -ErrorAction Stop

        if ($result.GetType().Name -eq "String") {
            ConvertFrom-Json -InputObject $result
        } else {
            $result
        }
    }
    Catch {
        Write-Error ("vCloud Availability API error: $($_.Exception.Message)")
        Break
    }
}

# An internal function to convert the supplied parameters into a query URL to
# be submitted against the VCAV API
Function New-VCAVUrl {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][string]$QueryPath,
        [Parameter()][hashtable]$Filter
    )

    $QueryString = "https://$Script:VCAVHost/$QueryPath"

    if ($Filter) {
        $FirstParam = $true
        foreach ($key in $Filter.Keys) {
            $QueryString += if ($FirstParam) { "?" } Else { "&" }
            $FirstParam = $false
            $QueryString += "$key=$($Filter.Item($key))"
        }
    }
    return $QueryString
}

# Export the public functions from this module to the environment:
Export-ModuleMember -Function Connect-VCAV
Export-ModuleMember -Function Disconnect-VCAV
Export-ModuleMember -Function Connect-VCAVExtend
Export-ModuleMember -Function Invoke-VCAVQuery
Export-ModuleMember -Function Invoke-VCAVPagedQuery
Export-ModuleMember -Function Get-VCAVToken