Skip to content

Commit

Permalink
Openapi TestConnection new method (#501)
Browse files Browse the repository at this point in the history
  • Loading branch information
Didainius authored Aug 29, 2022
1 parent f1bd3a0 commit df3370a
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 10 deletions.
6 changes: 6 additions & 0 deletions .changes/v2.17.0/501-features.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
* Add `Client.TestConnection` method to check remote VCD endpoints [GH-447], [GH-501]
* Add `Client.TestConnectionWithDefaults` method that uses `Client.TestConnection` with some default
values [GH-447], [GH-501]
* Change behavior of `Client.OpenApiPostItem` and `Client.OpenApiPostItemSync` so they accept
response code 200 OK as valid. The reason is `TestConnection` endpoint requires a POST request and
returns a 200OK when successful [GH-447], [GH-501]
79 changes: 79 additions & 0 deletions govcd/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"net/url"
"os"
"regexp"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -778,6 +779,84 @@ func getAdminURL(href string) string {
return strings.ReplaceAll(href, "/api/", "/api/admin/")
}

// TestConnection calls API to test a connection against a VCD, including SSL handshake and hostname verification.
func (client *Client) TestConnection(testConnection types.TestConnection) (*types.TestConnectionResult, error) {
endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointTestConnection

apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint)
if err != nil {
return nil, err
}

urlRef, err := client.OpenApiBuildEndpoint(endpoint)
if err != nil {
return nil, err
}

returnTestConnectionResult := &types.TestConnectionResult{
TargetProbe: &types.ProbeResult{},
ProxyProbe: &types.ProbeResult{},
}

err = client.OpenApiPostItem(apiVersion, urlRef, nil, testConnection, returnTestConnectionResult, nil)
if err != nil {
return nil, fmt.Errorf("error performing test connection: %s", err)
}

return returnTestConnectionResult, nil
}

// TestConnectionWithDefaults calls TestConnection given a subscriptionURL. The rest of parameters are set as default.
// It returns whether it could reach the server and establish SSL connection or not.
func (client *Client) TestConnectionWithDefaults(subscriptionURL string) (bool, error) {
if subscriptionURL == "" {
return false, fmt.Errorf("TestConnectionWithDefaults needs to be passed a host. i.e. my-host.vmware.com")
}

url, err := url.Parse(subscriptionURL)
if err != nil {
return false, fmt.Errorf("unable to parse URL - %s", err)
}

// Get port
var port int
if v := url.Port(); v != "" {
port, err = strconv.Atoi(v)
if err != nil {
return false, fmt.Errorf("couldn't parse port provided - %s", err)
}
} else {
switch url.Scheme {
case "http":
port = 80
case "https":
port = 443
}
}

testConnectionConfig := types.TestConnection{
Host: url.Hostname(),
Port: port,
Secure: takeBoolPointer(true), // Default value used by VCD UI
Timeout: 30, // Default value used by VCD UI
}

testConnectionResult, err := client.TestConnection(testConnectionConfig)
if err != nil {
return false, err
}

if !testConnectionResult.TargetProbe.CanConnect {
return false, fmt.Errorf("the remote host is not reachable")
}

if !testConnectionResult.TargetProbe.SSLHandshake {
return true, fmt.Errorf("unsupported or unrecognized SSL message")
}

return true, nil
}

// ---------------------------------------------------------------------
// The following functions are needed to avoid strict Coverity warnings
// ---------------------------------------------------------------------
Expand Down
13 changes: 7 additions & 6 deletions govcd/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ func (client *Client) OpenApiGetItem(apiVersion string, urlRef *url.URL, params

// OpenApiPostItemSync is a low level OpenAPI client function to perform POST request for items that support synchronous
// requests. The urlRef must point to POST endpoint (e.g. '/1.0.0/edgeGateways') that supports synchronous requests. It
// will return an error when endpoint does not support synchronous requests (HTTP response status code is not 201).
// will return an error when endpoint does not support synchronous requests (HTTP response status code is not 200 or 201).
// Response will be unmarshalled into outType.
//
// Note. Even though it may return error if the item does not support synchronous request - the object may still be
Expand All @@ -187,8 +187,9 @@ func (client *Client) OpenApiPostItemSync(apiVersion string, urlRef *url.URL, pa
return err
}

if resp.StatusCode != http.StatusCreated {
util.Logger.Printf("[TRACE] Synchronous task expected (HTTP status code 201). Got %d", resp.StatusCode)
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
util.Logger.Printf("[TRACE] Synchronous task expected (HTTP status code 200 or 201). Got %d", resp.StatusCode)
return fmt.Errorf("POST request expected sync task (HTTP response 200 or 201), got %d", resp.StatusCode)

}

Expand Down Expand Up @@ -265,7 +266,7 @@ func (client *Client) OpenApiPostItem(apiVersion string, urlRef *url.URL, params
return err
}

// Handle two cases of API behaviour - synchronous (response status code is 201) and asynchronous (response status
// Handle two cases of API behaviour - synchronous (response status code is 200 or 201) and asynchronous (response status
// code 202)
switch resp.StatusCode {
// Asynchronous case - must track task and get item HREF from there
Expand All @@ -290,8 +291,8 @@ func (client *Client) OpenApiPostItem(apiVersion string, urlRef *url.URL, params
}

// Synchronous task - new item body is returned in response of HTTP POST request
case http.StatusCreated:
util.Logger.Printf("[TRACE] Synchronous task detected, marshalling outType '%s'", reflect.TypeOf(outType))
case http.StatusCreated, http.StatusOK:
util.Logger.Printf("[TRACE] Synchronous task detected (HTTP Status %d), marshalling outType '%s'", resp.StatusCode, reflect.TypeOf(outType))
if err = decodeBody(types.BodyTypeJSON, resp, outType); err != nil {
return fmt.Errorf("error decoding JSON response after POST: %s", err)
}
Expand Down
1 change: 1 addition & 0 deletions govcd/openapi_endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ var endpointMinApiVersions = map[string]string{
types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRoles + types.OpenApiEndpointRights: "31.0",
types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAuditTrail: "33.0",
types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointImportableTier0Routers: "32.0",
types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointTestConnection: "34.0",
// OpenApiEndpointExternalNetworks endpoint support was introduced with version 32.0 however it was still not stable
// enough to be used. (i.e. it did not support update "PUT")
types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointExternalNetworks: "33.0",
Expand Down
65 changes: 65 additions & 0 deletions govcd/openapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package govcd

import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"regexp"
Expand Down Expand Up @@ -133,6 +134,7 @@ func (vcd *TestVCD) Test_OpenApiInlineStructAuditTrail(check *C) {
// 5. Deletes created role
// 6. Tests read for deleted item
// 7. Create role once more using "Sync" version of POST function
// 7.1 Queries TestConnection endpoint using "Sync" version of POST function to see that it handles 200OK accordingly
// 8. Update role once more using "Sync" version of PUT function
// 9. Delete role once again
func (vcd *TestVCD) Test_OpenApiInlineStructCRUDRoles(check *C) {
Expand Down Expand Up @@ -240,6 +242,25 @@ func (vcd *TestVCD) Test_OpenApiInlineStructCRUDRoles(check *C) {
newRole.ID = newRoleResponse.ID
check.Assert(newRoleResponse, DeepEquals, newRole)

// Step 7.1 test synchronous POST with return code 200 OK works accordingly - This is checked because OpenAPI endpoint TestConnection returns 200 instead of 201 when success
var testConnectionResult types.TestConnectionResult
testConnectionPayload := types.TestConnection{
Host: vcd.client.Client.VCDHREF.Host,
Port: 443,
Secure: takeBoolPointer(true),
Timeout: 10,
}

testConnectionEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointTestConnection
apiVersionTestConnection, err := vcd.client.Client.checkOpenApiEndpointCompatibility(testConnectionEndpoint)
check.Assert(err, IsNil)

urlRefTestConnection, err := vcd.client.Client.OpenApiBuildEndpoint(testConnectionEndpoint)
check.Assert(err, IsNil)

err = vcd.client.Client.OpenApiPostItemSync(apiVersionTestConnection, urlRefTestConnection, nil, testConnectionPayload, &testConnectionResult) // This call will get a 200 OK, which is what is being tested here
check.Assert(err, IsNil)

// Step 8 - update role using synchronous PUT function
newRoleResponse.Description = "Updated description created by sync test"
updateUrl2, err := vcd.client.Client.OpenApiBuildEndpoint(endpoint, newRoleResponse.ID)
Expand All @@ -261,6 +282,50 @@ func (vcd *TestVCD) Test_OpenApiInlineStructCRUDRoles(check *C) {

}

func (vcd *TestVCD) Test_OpenApiTestConnection(check *C) {
// TestConnection is going to be used against the same VCD instance as the client is connected
urlTest1 := vcd.client.Client.VCDHREF
urlTest1.Path = "vcsp/lib/a0c959b4-a6dd-4a68-8042-5025f42d845e"
urlTest2 := vcd.client.Client.VCDHREF
urlTest2.Scheme = "http"
urlTest3 := vcd.client.Client.VCDHREF
urlTest3.Host = "imadethisup.io"
urlTest4 := vcd.client.Client.VCDHREF
urlTest4.Host = fmt.Sprintf("%s:666", urlTest4.Hostname()) // For testing custom port feature
tests := []struct {
SubscriptionURL string
WantedConnection bool
WantedError bool
}{
{
SubscriptionURL: urlTest1.String(),
WantedConnection: true, // it connects and it does SSL connection
WantedError: false, //
},
{
SubscriptionURL: urlTest2.String(),
WantedConnection: true, // it connects but it does not do SSL connection
WantedError: true,
},
{
SubscriptionURL: urlTest3.String(), // it doesn't do neither connection nor SSL
WantedConnection: false,
WantedError: true,
},
{
SubscriptionURL: urlTest4.String(), // it doesn't do neither connection nor SSL but tests custom port
WantedConnection: false,
WantedError: true,
},
}

for _, test := range tests {
result, err := vcd.client.Client.TestConnectionWithDefaults(test.SubscriptionURL)
check.Assert(err == nil, Equals, !test.WantedError)
check.Assert(result, Equals, test.WantedConnection)
}
}

// getAuditTrailTimestampWithElements helps to pick good timestamp filter so that it doesn't take long time to retrieve
// too many items
func getAuditTrailTimestampWithElements(elementCount int, check *C, vcd *TestVCD, apiVersion string, urlRef *url.URL) string {
Expand Down
8 changes: 4 additions & 4 deletions types/v56/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -376,10 +376,10 @@ const (
OpenApiEndpointNetworkContextProfiles = "networkContextProfiles"
OpenApiEndpointSecurityTags = "securityTags"
OpenApiEndpointNsxtRouteAdvertisement = "edgeGateways/%s/routing/advertisement"

OpenApiEndpointEdgeBgpNeighbor = "edgeGateways/%s/routing/bgp/neighbors/" // '%s' is NSX-T Edge Gateway ID
OpenApiEndpointEdgeBgpConfigPrefixLists = "edgeGateways/%s/routing/bgp/prefixLists/" // '%s' is NSX-T Edge Gateway ID
OpenApiEndpointEdgeBgpConfig = "edgeGateways/%s/routing/bgp" // '%s' is NSX-T Edge Gateway ID
OpenApiEndpointTestConnection = "testConnection/"
OpenApiEndpointEdgeBgpNeighbor = "edgeGateways/%s/routing/bgp/neighbors/" // '%s' is NSX-T Edge Gateway ID
OpenApiEndpointEdgeBgpConfigPrefixLists = "edgeGateways/%s/routing/bgp/prefixLists/" // '%s' is NSX-T Edge Gateway ID
OpenApiEndpointEdgeBgpConfig = "edgeGateways/%s/routing/bgp" // '%s' is NSX-T Edge Gateway ID

// NSX-T ALB related endpoints

Expand Down
39 changes: 39 additions & 0 deletions types/v56/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -365,3 +365,42 @@ type DefaultPolicy struct {
type VersionField struct {
Version int `json:"version"`
}

// TestConnection defines the parameters used when testing a connection, including SSL handshake and hostname verification.
type TestConnection struct {
Host string `json:"host"` // The host (or IP address) to connect to.
Port int `json:"port"` // The port to use when connecting.
Secure *bool `json:"secure,omitempty"` // If the connection should use https.
Timeout int `json:"timeout,omitempty"` // Maximum time (in seconds) any step in the test should wait for a response.
HostnameVerificationAlgorithm string `json:"hostnameVerificationAlgorithm,omitempty"` // Endpoint/Hostname verification algorithm to be used during SSL/TLS/DTLS handshake.
AdditionalCAIssuers []string `json:"additionalCAIssuers,omitempty"` // A list of URLs being authorized by the user to retrieve additional CA certificates from, if necessary, to complete the certificate chain to its trust anchor.
ProxyConnection *ProxyTestConnection `json:"proxyConnection,omitempty"` // Proxy connection to use for test. Only one of proxyConnection and preConfiguredProxy can be specified.
PreConfiguredProxy string `json:"preConfiguredProxy,omitempty"` // The URN of a ProxyConfiguration to use for the test. Only one of proxyConnection or preConfiguredProxy can be specified. If neither is specified then no proxy is used to test the connection.
}

// ProxyTestConnection defines the proxy connection to use for TestConnection (if any).
type ProxyTestConnection struct {
ProxyHost string `json:"proxyHost"` // The host (or IP address) of the proxy.
ProxyPort int `json:"proxyPort"` // The port to use when connecting to the proxy.
ProxyUsername string `json:"proxyUsername,omitempty"` // Username to authenticate to the proxy.
ProxyPassword string `json:"proxyPassword,omitempty"` // Password to authenticate to the proxy.
ProxySecure *bool `json:"proxySecure,omitempty"` // If the connection to the proxy should use https.
}

// TestConnectionResult is the result of a connection test.
type TestConnectionResult struct {
TargetProbe *ProbeResult `json:"targetProbe,omitempty"` // Results of a connection test to a specific endpoint.
ProxyProbe *ProbeResult `json:"proxyProbe,omitempty"` // Results of a connection test to a specific endpoint.
}

// ProbeResult results of a connection test to a specific endpoint.
type ProbeResult struct {
Result string `json:"result,omitempty"` // Localized message describing the connection result stating success or an error message with a brief summary.
ResolvedIp string `json:"resolvedIp,omitempty"` // The IP address the host was resolved to, if not going through a proxy.
CanConnect bool `json:"canConnect,omitempty"` // If vCD can establish a connection on the specified port.
SSLHandshake bool `json:"sslHandshake,omitempty"` // If an SSL Handshake succeeded (secure requests only).
ConnectionResult string `json:"connectionResult,omitempty"` // A code describing the result of establishing a connection. It can be either SUCCESS, ERROR_CANNOT_RESOLVE_IP or ERROR_CANNOT_CONNECT.
SSLResult string `json:"sslResult,omitempty"` // A code describing the result of the SSL handshake. It can be either SUCCESS, ERROR_SSL_ERROR, ERROR_UNTRUSTED_CERTIFICATE, ERROR_CANNOT_VERIFY_HOSTNAME or null.
CertificateChain string `json:"certificateChain,omitempty"` // The SSL certificate chain presented by the server if a secure connection was made.
AdditionalCAIssuers []string `json:"additionalCAIssuers,omitempty"` // URLs supplied by Certificate Authorities to retrieve signing certificates, when those certificates are not included in the chain.
}

0 comments on commit df3370a

Please sign in to comment.