diff --git a/.gitignore b/.gitignore index b5b8b364d..88dc8cec9 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,8 @@ secrets.yml # Pycharm IDE .idea +# VScode IDE +.vscode # Test artifacts govcd/govcd_test_config.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 146d30634..50b86875d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 2.3.0 (Unreleased) * Added edge gateway create/delete functions [#130](https://github.com/vmware/go-vcloud-director/issues/130). +* Added load balancer service monitor [#196](https://github.com/vmware/go-vcloud-director/pull/196) ## 2.2.0 (May 15, 2019) diff --git a/go.mod b/go.mod index 1c75c93e1..63d608087 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/vmware/go-vcloud-director/v2 require ( github.com/hashicorp/go-version v1.1.0 - github.com/kr/pretty v0.1.0 // indirect + github.com/kr/pretty v0.1.0 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 gopkg.in/yaml.v2 v2.2.2 ) diff --git a/govcd/api.go b/govcd/api.go index 87265b8e8..ae376927e 100644 --- a/govcd/api.go +++ b/govcd/api.go @@ -9,6 +9,7 @@ import ( "bytes" "encoding/xml" "fmt" + "io" "io/ioutil" "net/http" @@ -96,18 +97,16 @@ func (cli *Client) NewRequest(params map[string]string, method string, reqUrl ur return cli.NewRequestWitNotEncodedParams(params, nil, method, reqUrl, body) } -// ParseErr takes an error XML resp and returns a single string for use in error messages. -func ParseErr(resp *http.Response) error { - - errBody := new(types.Error) - +// ParseErr takes an error XML resp, error interface for unmarshaling and returns a single string for +// use in error messages. +func ParseErr(resp *http.Response, errType error) error { // if there was an error decoding the body, just return that - if err := decodeBody(resp, errBody); err != nil { + if err := decodeBody(resp, errType); err != nil { util.Logger.Printf("[ParseErr]: unhandled response <--\n%+v\n-->\n", resp) return fmt.Errorf("[ParseErr]: error parsing error body for non-200 request: %s (%+v)", err, resp) } - return fmt.Errorf("API Error: %d: %s", errBody.MajorErrorCode, errBody.Message) + return errType } // decodeBody is used to XML decode a response body @@ -133,6 +132,12 @@ func decodeBody(resp *http.Response, out interface{}) error { // parses the resultant XML error and returns a descriptive error, if the // status code is not handled it returns a generic error with the status code. func checkResp(resp *http.Response, err error) (*http.Response, error) { + return checkRespWithErrType(resp, err, &types.Error{}) +} + +// checkRespWithErrType allows to specify custom error errType for checkResp unmarshaling +// the error. +func checkRespWithErrType(resp *http.Response, err, errType error) (*http.Response, error) { if err != nil { return resp, err } @@ -172,7 +177,7 @@ func checkResp(resp *http.Response, err error) (*http.Response, error) { http.StatusInternalServerError, // 500 http.StatusServiceUnavailable, // 503 http.StatusGatewayTimeout: // 504 - return nil, ParseErr(resp) + return nil, ParseErr(resp, errType) // Unhandled response. default: return nil, fmt.Errorf("unhandled API response, please report this issue, status code: %s", resp.Status) @@ -230,6 +235,9 @@ func (client *Client) ExecuteRequestWithoutResponse(pathURL, requestType, conten return fmt.Errorf(errorMessage, err) } + // log response explicitly because decodeBody() was not triggered + util.ProcessResponseOutput(util.FuncNameCallStack(), resp, fmt.Sprintf("%s", resp.Body)) + err = resp.Body.Close() if err != nil { return fmt.Errorf("error closing response body: %s", err) @@ -272,7 +280,30 @@ func (client *Client) ExecuteRequest(pathURL, requestType, contentType, errorMes return resp, nil } +// ExecuteRequestWithCustomError sends the request and checks for 2xx response. If the returned status code +// was not as expected - the returned error will be unmarshaled to `errType` which implements Go's standard `error` +// interface. +func (client *Client) ExecuteRequestWithCustomError(pathURL, requestType, contentType, errorMessage string, + payload interface{}, errType error) (*http.Response, error) { + if !isMessageWithPlaceHolder(errorMessage) { + return &http.Response{}, fmt.Errorf("error message has to include place holder for error") + } + + resp, err := executeRequestCustomErr(pathURL, requestType, contentType, payload, client, errType) + if err != nil { + return &http.Response{}, fmt.Errorf(errorMessage, err) + } + + return resp, nil +} + +// executeRequest does executeRequestCustomErr and checks for vCD errors in API response func executeRequest(pathURL, requestType, contentType string, payload interface{}, client *Client) (*http.Response, error) { + return executeRequestCustomErr(pathURL, requestType, contentType, payload, client, &types.Error{}) +} + +// executeRequestCustomErr performs request and unmarshals API error to errType if not 2xx status was returned +func executeRequestCustomErr(pathURL, requestType, contentType string, payload interface{}, client *Client, errType error) (*http.Response, error) { url, _ := url.ParseRequestURI(pathURL) var req *http.Request @@ -295,7 +326,12 @@ func executeRequest(pathURL, requestType, contentType string, payload interface{ req.Header.Add("Content-Type", contentType) } - return checkResp(client.Http.Do(req)) + resp, err := client.Http.Do(req) + if err != nil { + return resp, err + } + + return checkRespWithErrType(resp, err, errType) } func isMessageWithPlaceHolder(message string) bool { diff --git a/govcd/api_vcd_test.go b/govcd/api_vcd_test.go index 6cc7fbcdb..505ca69d4 100644 --- a/govcd/api_vcd_test.go +++ b/govcd/api_vcd_test.go @@ -57,6 +57,7 @@ const ( TestVMDetachDisk = "TestVMDetachDisk" TestCreateExternalNetwork = "TestCreateExternalNetwork" TestDeleteExternalNetwork = "TestDeleteExternalNetwork" + Test_LBServiceMonitor = "Test_LBServiceMonitor" ) const ( @@ -328,13 +329,18 @@ func (vcd *TestVCD) infoCleanup(format string, args ...interface{}) { } // Gets the two components of a "parent" string, as passed to AddToCleanupList -func splitParent(parent string, separator string) (first, second string) { +func splitParent(parent string, separator string) (first, second, third string) { strList := strings.Split(parent, separator) - if len(strList) != 2 { - return "", "" + if len(strList) < 2 && len(strList) > 3 { + return "", "", "" } first = strList[0] second = strList[1] + + if len(strList) == 3 { + third = strList[2] + } + return } @@ -342,7 +348,7 @@ var splitParentNotFound string = "removeLeftoverEntries: [ERROR] missing parent var notFoundMsg string = "removeLeftoverEntries: [INFO] No action for %s '%s'\n" func (vcd *TestVCD) getAdminOrgAndVdcFromCleanupEntity(entity CleanupEntity) (org AdminOrg, vdc Vdc, err error) { - orgName, vdcName := splitParent(entity.Parent, "|") + orgName, vdcName, _ := splitParent(entity.Parent, "|") if orgName == "" || vdcName == "" { vcd.infoCleanup(splitParentNotFound, entity.Parent) return AdminOrg{}, Vdc{}, fmt.Errorf("can't find parents names") @@ -617,6 +623,37 @@ func (vcd *TestVCD) removeLeftoverEntities(entity CleanupEntity) { vcd.infoCleanup(removedMsg, entity.EntityType, entity.Name, entity.CreatedBy) return + case "lbServiceMonitor": + if entity.Parent == "" { + vcd.infoCleanup("removeLeftoverEntries: [ERROR] No parent specified '%s'\n", entity.Name) + return + } + + orgName, vdcName, edgeName := splitParent(entity.Parent, "|") + + org, err := GetOrgByName(vcd.client, orgName) + if err != nil { + vcd.infoCleanup("removeLeftoverEntries: [ERROR] Could not find org '%s'\n", orgName) + } + vdc, err := org.GetVdcByName(vdcName) + if err != nil { + vcd.infoCleanup("removeLeftoverEntries: [ERROR] Could not find vdc '%s'\n", vdcName) + } + + edge, err := vdc.FindEdgeGateway(edgeName) + if err != nil { + vcd.infoCleanup("removeLeftoverEntries: [ERROR] Could not find edge '%s'\n", vdcName) + } + + err = edge.DeleteLBServiceMonitor(&types.LBMonitor{Name: entity.Name}) + if err != nil { + vcd.infoCleanup(notFoundMsg, entity.EntityType, entity.Name) + return + } + + vcd.infoCleanup(removedMsg, entity.EntityType, entity.Name, entity.CreatedBy) + return + default: // If we reach this point, we are trying to clean up an entity that // we aren't prepared for yet. @@ -729,3 +766,33 @@ func (vcd *TestVCD) createTestVapp(name string) (VApp, error) { func init() { testingTags["api"] = "api_vcd_test.go" } + +func Test_splitParent(t *testing.T) { + type args struct { + parent string + separator string + } + tests := []struct { + name string + args args + wantFirst string + wantSecond string + wantThird string + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotFirst, gotSecond, gotThird := splitParent(tt.args.parent, tt.args.separator) + if gotFirst != tt.wantFirst { + t.Errorf("splitParent() gotFirst = %v, want %v", gotFirst, tt.wantFirst) + } + if gotSecond != tt.wantSecond { + t.Errorf("splitParent() gotSecond = %v, want %v", gotSecond, tt.wantSecond) + } + if gotThird != tt.wantThird { + t.Errorf("splitParent() gotThird = %v, want %v", gotThird, tt.wantThird) + } + }) + } +} diff --git a/govcd/edgegateway.go b/govcd/edgegateway.go index 243428c92..ace1efc3b 100644 --- a/govcd/edgegateway.go +++ b/govcd/edgegateway.go @@ -727,3 +727,28 @@ func (egw *EdgeGateway) HasDefaultGateway() bool { } return false } + +// HasAdvancedNetworking returns true if the edge gateway has advanced network configuration enabled +func (egw *EdgeGateway) HasAdvancedNetworking() bool { + return egw.EdgeGateway.Configuration != nil && egw.EdgeGateway.Configuration.AdvancedNetworkingEnabled +} + +// buildProxiedEdgeEndpointURL helps to get root endpoint for Edge Gateway using the +// NSX API Proxy and can append optionalSuffix which must have its own leading / +func (eGW *EdgeGateway) buildProxiedEdgeEndpointURL(optionalSuffix string) (string, error) { + apiEndpoint, err := url.ParseRequestURI(eGW.EdgeGateway.HREF) + if err != nil { + return "", fmt.Errorf("unable to process edge gateway URL: %s", err) + } + edgeID := strings.Split(eGW.EdgeGateway.ID, ":") + if len(edgeID) != 4 { + return "", fmt.Errorf("unable to find edge gateway id: %s", eGW.EdgeGateway.ID) + } + hostname := apiEndpoint.Scheme + "://" + apiEndpoint.Host + "/network/edges/" + edgeID[3] + + if optionalSuffix != "" { + return hostname + optionalSuffix, nil + } + + return hostname, nil +} diff --git a/govcd/lbservicemonitor.go b/govcd/lbservicemonitor.go new file mode 100644 index 000000000..cea3a4ead --- /dev/null +++ b/govcd/lbservicemonitor.go @@ -0,0 +1,200 @@ +/* + * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "net/http" + "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// CreateLBServiceMonitor creates a load balancer service monitor based on mandatory fields. It is a synchronous +// operation. It returns created object with all fields (including ID) populated or an error. +func (eGW *EdgeGateway) CreateLBServiceMonitor(lbMonitorConfig *types.LBMonitor) (*types.LBMonitor, error) { + if err := validateCreateLBServiceMonitor(lbMonitorConfig); err != nil { + return nil, err + } + + if !eGW.HasAdvancedNetworking() { + return nil, fmt.Errorf("edge gateway does not have advanced networking enabled") + } + + httpPath, err := eGW.buildProxiedEdgeEndpointURL(types.LBMonitorPath) + if err != nil { + return nil, fmt.Errorf("could not get Edge Gateway API endpoint: %s", err) + } + // We expect to get http.StatusCreated or if not an error of type types.NSXError + resp, err := eGW.client.ExecuteRequestWithCustomError(httpPath, http.MethodPost, types.AnyXMLMime, + "error creating load balancer service monitor: %s", lbMonitorConfig, &types.NSXError{}) + if err != nil { + return nil, err + } + location := resp.Header.Get("Location") + + // Last element in location header is the service monitor ID + // i.e. Location: [/network/edges/edge-3/loadbalancer/config/monitors/monitor-5] + // The code below extracts that ID from the last segment + if location == "" { + return nil, fmt.Errorf("unable to retrieve ID for new load balancer service monitor with name %s", lbMonitorConfig.Name) + } + splitLocation := strings.Split(location, "/") + lbMonitorID := splitLocation[len(splitLocation)-1] + + readMonitor, err := eGW.ReadLBServiceMonitor(&types.LBMonitor{ID: lbMonitorID}) + if err != nil { + return nil, fmt.Errorf("unable to retrieve monitor with ID (%s) after creation: %s", readMonitor.ID, err) + } + return readMonitor, nil +} + +// ReadLBServiceMonitor is able to find the types.LBMonitor type by Name and/or ID. +// If both - Name and ID are specified it performs a lookup by ID and returns an error if the specified name and found +// name do not match. +func (eGW *EdgeGateway) ReadLBServiceMonitor(lbMonitorConfig *types.LBMonitor) (*types.LBMonitor, error) { + if err := validateReadLBServiceMonitor(lbMonitorConfig); err != nil { + return nil, err + } + + httpPath, err := eGW.buildProxiedEdgeEndpointURL(types.LBMonitorPath) + if err != nil { + return nil, fmt.Errorf("could not get Edge Gateway API endpoint: %s", err) + } + + // Anonymous struct to unwrap "monitor response" + lbMonitorResponse := &struct { + LBMonitors []*types.LBMonitor `xml:"monitor"` + }{} + + // This query returns all service monitors as the API does not have filtering options + _, err = eGW.client.ExecuteRequest(httpPath, http.MethodGet, types.AnyXMLMime, "unable to read Load Balancer monitor: %s", nil, lbMonitorResponse) + if err != nil { + return nil, err + } + + // Search for monitor by ID or by Name + for _, monitor := range lbMonitorResponse.LBMonitors { + // If ID was specified for lookup - look for the same ID + if lbMonitorConfig.ID != "" && monitor.ID == lbMonitorConfig.ID { + return monitor, nil + } + + // If Name was specified for lookup - look for the same Name + if lbMonitorConfig.Name != "" && monitor.Name == lbMonitorConfig.Name { + // We found it by name. Let's verify if search ID was specified and it matches the lookup object + if lbMonitorConfig.ID != "" && monitor.ID != lbMonitorConfig.ID { + return nil, fmt.Errorf("load balancer monitor was found by name (%s), but it's ID (%s) does not match specified ID (%s)", + monitor.Name, monitor.ID, lbMonitorConfig.ID) + } + return monitor, nil + } + } + + return nil, fmt.Errorf("could not find load balancer service monitor (name: %s, ID: %s)", + lbMonitorConfig.Name, lbMonitorConfig.ID) +} + +// UpdateLBServiceMonitor +func (eGW *EdgeGateway) UpdateLBServiceMonitor(lbMonitorConfig *types.LBMonitor) (*types.LBMonitor, error) { + if err := validateUpdateLBServiceMonitor(lbMonitorConfig); err != nil { + return nil, err + } + + // if only name was specified for update, ID must be found, because ID is mandatory for update + if lbMonitorConfig.ID == "" { + readLBMonitor, err := eGW.ReadLBServiceMonitor(&types.LBMonitor{Name: lbMonitorConfig.Name}) + if err != nil { + return nil, err + } + lbMonitorConfig.ID = readLBMonitor.ID + } + + httpPath, err := eGW.buildProxiedEdgeEndpointURL(types.LBMonitorPath + lbMonitorConfig.ID) + if err != nil { + return nil, fmt.Errorf("could not get Edge Gateway API endpoint: %s", err) + } + + // Result should be 204, if not we expect an error of type types.NSXError + _, err = eGW.client.ExecuteRequestWithCustomError(httpPath, http.MethodPut, types.AnyXMLMime, + "error while updating load balancer service monitor : %s", lbMonitorConfig, &types.NSXError{}) + if err != nil { + return nil, err + } + + readMonitor, err := eGW.ReadLBServiceMonitor(&types.LBMonitor{ID: lbMonitorConfig.ID}) + if err != nil { + return nil, fmt.Errorf("unable to retrieve monitor with ID (%s) after update: %s", readMonitor.ID, err) + } + return readMonitor, nil +} + +// DeleteLBServiceMonitor is able to delete the types.LBMonitor type by Name and/or ID. +// If both - Name and ID are specified it performs a lookup by ID and returns an error if the specified name and found +// name do not match. +func (eGW *EdgeGateway) DeleteLBServiceMonitor(lbMonitorConfig *types.LBMonitor) error { + if err := validateDeleteLBServiceMonitor(lbMonitorConfig); err != nil { + return err + } + + lbMonitorID := lbMonitorConfig.ID + // if only name was specified for deletion, ID must be found, because only ID can be used for deletion + if lbMonitorConfig.ID == "" { + readLBMonitor, err := eGW.ReadLBServiceMonitor(&types.LBMonitor{Name: lbMonitorConfig.Name}) + if err != nil { + return fmt.Errorf("unable to find load balancer monitor by name for deletion: %s", err) + } + lbMonitorID = readLBMonitor.ID + } + + httpPath, err := eGW.buildProxiedEdgeEndpointURL(types.LBMonitorPath + lbMonitorID) + if err != nil { + return fmt.Errorf("could not get Edge Gateway API endpoint: %s", err) + } + return eGW.client.ExecuteRequestWithoutResponse(httpPath, http.MethodDelete, types.AnyXMLMime, + "unable to delete Service Monitor: %s", nil) +} + +func validateCreateLBServiceMonitor(lbMonitorConfig *types.LBMonitor) error { + if lbMonitorConfig.Name == "" { + return fmt.Errorf("load balancer monitor Name cannot be empty") + } + + if lbMonitorConfig.Timeout == 0 { + return fmt.Errorf("load balancer monitor Timeout cannot be 0") + } + + if lbMonitorConfig.Interval == 0 { + return fmt.Errorf("load balancer monitor Interval cannot be 0") + } + + if lbMonitorConfig.MaxRetries == 0 { + return fmt.Errorf("load balancer monitor MaxRetries cannot be 0") + } + + if lbMonitorConfig.Type == "" { + return fmt.Errorf("load balancer monitor Type cannot be empty") + } + + return nil +} + +func validateReadLBServiceMonitor(lbMonitorConfig *types.LBMonitor) error { + if lbMonitorConfig.ID == "" && lbMonitorConfig.Name == "" { + return fmt.Errorf("to read load balancer service monitor at least one of `ID`, `Name` fields must be specified") + } + + return nil +} + +func validateUpdateLBServiceMonitor(lbMonitorConfig *types.LBMonitor) error { + // Update and create have the same requirements for now + return validateCreateLBServiceMonitor(lbMonitorConfig) +} + +func validateDeleteLBServiceMonitor(lbMonitorConfig *types.LBMonitor) error { + // Read and delete have the same requirements for now + return validateReadLBServiceMonitor(lbMonitorConfig) +} diff --git a/govcd/lbservicemonitor_test.go b/govcd/lbservicemonitor_test.go new file mode 100644 index 000000000..6db5d8af9 --- /dev/null +++ b/govcd/lbservicemonitor_test.go @@ -0,0 +1,83 @@ +// +build lb lbServiceMonitor functional ALL + +/* + * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +// Test_LBServiceMonitor tests CRUD methods for load balancer service monitor. +// The following things are tested: +// Creation of load balancer service monitor +// Read load balancer by both ID and Name (service monitor name must be unique in single edge gateway) +// Update - change a single field and compare that configuration and result objects are deeply equal +// Update - try and fail to update without mandatory field +// Delete +func (vcd *TestVCD) Test_LBServiceMonitor(check *C) { + if vcd.config.VCD.EdgeGateway == "" { + check.Skip("Skipping test because no edge gateway given") + } + edge, err := vcd.vdc.FindEdgeGateway(vcd.config.VCD.EdgeGateway) + check.Assert(err, IsNil) + check.Assert(edge.EdgeGateway.Name, Equals, vcd.config.VCD.EdgeGateway) + + if !edge.HasAdvancedNetworking() { + check.Skip("Skipping test because the edge gateway does not have advanced networking enabled") + } + + // Used for creating + lbMon := &types.LBMonitor{ + Name: check.TestName(), + Interval: 10, + Timeout: 10, + MaxRetries: 3, + Type: "http", + } + + lbMonitor, err := edge.CreateLBServiceMonitor(lbMon) + check.Assert(err, IsNil) + check.Assert(lbMonitor.ID, Not(IsNil)) + + // We created monitor successfully therefore let's add it to cleanup list + parentEntity := vcd.org.Org.Name + "|" + vcd.vdc.Vdc.Name + "|" + vcd.config.VCD.EdgeGateway + AddToCleanupList(check.TestName(), "lbServiceMonitor", parentEntity, check.TestName()) + + // Lookup by both name and ID and compare that these are equal values + lbMonitorByID, err := edge.ReadLBServiceMonitor(&types.LBMonitor{ID: lbMonitor.ID}) + check.Assert(err, IsNil) + + lbMonitorByName, err := edge.ReadLBServiceMonitor(&types.LBMonitor{Name: lbMonitor.Name}) + check.Assert(err, IsNil) + check.Assert(lbMonitor.ID, Equals, lbMonitorByName.ID) + check.Assert(lbMonitorByID.ID, Equals, lbMonitorByName.ID) + check.Assert(lbMonitorByID.Name, Equals, lbMonitorByName.Name) + + check.Assert(lbMonitor.ID, Equals, lbMonitorByID.ID) + check.Assert(lbMonitor.Interval, Equals, lbMonitorByID.Interval) + check.Assert(lbMonitor.Timeout, Equals, lbMonitorByID.Timeout) + check.Assert(lbMonitor.MaxRetries, Equals, lbMonitorByID.MaxRetries) + + // Test updating fields + // Update timeout + lbMonitorByID.Timeout = 35 + updatedLBMonitor, err := edge.UpdateLBServiceMonitor(lbMonitorByID) + check.Assert(err, IsNil) + check.Assert(updatedLBMonitor.Timeout, Equals, 35) + + // Verify that updated monitor and it's configuration are identical + check.Assert(updatedLBMonitor, DeepEquals, lbMonitorByID) + + // Update should fail without name + lbMonitorByID.Name = "" + _, err = edge.UpdateLBServiceMonitor(lbMonitorByID) + check.Assert(err.Error(), Equals, "load balancer monitor Name cannot be empty") + + // Delete / cleanup + err = edge.DeleteLBServiceMonitor(&types.LBMonitor{ID: lbMonitorByID.ID}) + check.Assert(err, IsNil) +} diff --git a/govcd/system_test.go b/govcd/system_test.go index 543b937cc..380e51998 100644 --- a/govcd/system_test.go +++ b/govcd/system_test.go @@ -162,6 +162,7 @@ func (vcd *TestVCD) Test_CreateDeleteEdgeGateway(check *C) { util.Logger.Printf("Edge Gateway:\n%s\n", prettyEdgeGateway(*edge.EdgeGateway)) check.Assert(edge.HasDefaultGateway(), Equals, builtWithDefaultGateway) + check.Assert(edge.HasAdvancedNetworking(), Equals, egc.AdvancedNetworkingEnabled) // testing both delete methods if backingConf == "full" { diff --git a/govcd/vapp_test.go b/govcd/vapp_test.go index ff9ba2813..ae9c9960a 100644 --- a/govcd/vapp_test.go +++ b/govcd/vapp_test.go @@ -607,6 +607,12 @@ func (vcd *TestVCD) Test_AddNewVMMultiNIC(check *C) { // Cleanup err = vapp.RemoveVM(vm) check.Assert(err, IsNil) + + // Ensure network is detached from vApp to avoid conflicts in other tests + task, err = vapp.RemoveAllNetworks() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) } func verifyNetworkConnectionSection(check *C, actual, desired *types.NetworkConnectionSection) { diff --git a/govcd/vm.go b/govcd/vm.go index faa702023..37e357e54 100644 --- a/govcd/vm.go +++ b/govcd/vm.go @@ -539,7 +539,7 @@ func (vm *VM) GetQuestion() (types.VmPendingQuestion, error) { } if http.StatusOK != resp.StatusCode { - return types.VmPendingQuestion{}, fmt.Errorf("error getting question: %s", ParseErr(resp)) + return types.VmPendingQuestion{}, fmt.Errorf("error getting question: %s", ParseErr(resp, &types.Error{})) } question := &types.VmPendingQuestion{} diff --git a/types/v56/constants.go b/types/v56/constants.go index 53def40f7..474242b6f 100644 --- a/types/v56/constants.go +++ b/types/v56/constants.go @@ -15,6 +15,7 @@ const ( JSONMimeV57 = "application/json;version=5.7" // AnyXMLMime511 the wildcard xml mime for version 5.11 of the API AnyXMLMime511 = "application/*+xml;version=5.11" + AnyXMLMime = "application/xml" // Version511 the 5.11 version Version511 = "5.11" // Version is the default version number @@ -144,3 +145,7 @@ const ( XMLNamespaceVSSD = "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData" XMLNamespaceExtension = "http://www.vmware.com/vcloud/extension/v1.5" ) + +const ( + LBMonitorPath = "/loadbalancer/config/monitors/" +) diff --git a/types/v56/types.go b/types/v56/types.go index 4d48e37a5..7ac2b8ae5 100644 --- a/types/v56/types.go +++ b/types/v56/types.go @@ -6,6 +6,7 @@ package types import ( "encoding/xml" + "fmt" ) // Maps status Attribute Values for VAppTemplate, VApp, Vm, and Media Objects @@ -120,7 +121,6 @@ type NetworkFeatures struct { DhcpService *DhcpService `xml:"DhcpService,omitempty"` // Substitute for NetworkService. DHCP service settings FirewallService *FirewallService `xml:"FirewallService,omitempty"` // Substitute for NetworkService. Firewall service settings NatService *NatService `xml:"NatService,omitempty"` // Substitute for NetworkService. NAT service settings - LoadBalancerService *LoadBalancerService `xml:"LoadBalancerService,omitempty"` // Substitute for NetworkService. Load Balancer service settings StaticRoutingService *StaticRoutingService `xml:"StaticRoutingService,omitempty"` // Substitute for NetworkService. Static Routing service settings // TODO: Not Implemented // IpsecVpnService IpsecVpnService `xml:"IpsecVpnService,omitempty"` // Substitute for NetworkService. Ipsec Vpn service settings @@ -931,6 +931,25 @@ type Error struct { StackTrace string `xml:"stackTrace,attr,omitempty"` } +func (err Error) Error() string { + return fmt.Sprintf("API Error: %d: %s", err.MajorErrorCode, err.Message) +} + +// NSXError is the standard error message type used in the NSX API which is proxied by vCD. +// It has attached method `Error() string` and implements Go's default `type error` interface. +type NSXError struct { + XMLName xml.Name `xml:"error"` + ErrorCode string `xml:"errorCode"` + Details string `xml:"details"` + ModuleName string `xml:"moduleName"` +} + +// Error method implements Go's default `error` interface for NSXError and formats NSX error +// output for human readable output. +func (nsxErr NSXError) Error() string { + return fmt.Sprintf("%s %s (API error: %s)", nsxErr.ModuleName, nsxErr.Details, nsxErr.ErrorCode) +} + // File represents a file to be transferred (uploaded or downloaded). // Type: FileType // Namespace: http://www.vmware.com/vcloud/v1.5 @@ -1578,7 +1597,6 @@ type GatewayFeatures struct { NatService *NatService `xml:"NatService,omitempty"` // Substitute for NetworkService. NAT service settings GatewayDhcpService *GatewayDhcpService `xml:"GatewayDhcpService,omitempty"` // Substitute for NetworkService. Gateway DHCP service settings GatewayIpsecVpnService *GatewayIpsecVpnService `xml:"GatewayIpsecVpnService,omitempty"` // Substitute for NetworkService. Gateway Ipsec VPN service settings - LoadBalancerService *LoadBalancerService `xml:"LoadBalancerService,omitempty"` // Substitute for NetworkService. Load Balancer service settings StaticRoutingService *StaticRoutingService `xml:"StaticRoutingService,omitempty"` // Substitute for NetworkService. Static Routing service settings } @@ -1605,70 +1623,26 @@ type StaticRoute struct { GatewayInterface *Reference `xml:"GatewayInterface,omitempty"` // Gateway interface to which static route is bound. } -// LoadBalancerService represents gateway load balancer service. -// Type: LoadBalancerServiceType -// Namespace: http://www.vmware.com/vcloud/v1.5 -// Description: Represents gateway load balancer service. -// Since: 5.1 -type LoadBalancerService struct { - IsEnabled bool `xml:"IsEnabled"` // Enable or disable the service using this flag - Pool *LoadBalancerPool `xml:"Pool,omitempty"` // List of load balancer pools. - VirtualServer *LoadBalancerVirtualServer `xml:"VirtualServer,omitempty"` // List of load balancer virtual servers. -} - -// LoadBalancerPool represents a load balancer pool. -// Type: LoadBalancerPoolType -// Namespace: http://www.vmware.com/vcloud/v1.5 -// Description: Represents a load balancer pool. -// Since: 5.1 -type LoadBalancerPool struct { - ID string `xml:"Id,omitempty"` // Load balancer pool id. - Name string `xml:"Name"` // Load balancer pool name. - Description string `xml:"Description,omitempty"` // Load balancer pool description. - ServicePort *LBPoolServicePort `xml:"ServicePort"` // Load balancer pool service port. - Member *LBPoolMember `xml:"Member"` // Load balancer pool member. - Operational bool `xml:"Operational,omitempty"` // True if the load balancer pool is operational. - ErrorDetails string `xml:"ErrorDetails,omitempty"` // Error details for this pool. -} - -// LBPoolServicePort represents a service port in a load balancer pool. -// Type: LBPoolServicePortType -// Namespace: http://www.vmware.com/vcloud/v1.5 -// Description: Represents a service port in a load balancer pool. -// Since: 5.1 -type LBPoolServicePort struct { - IsEnabled bool `xml:"IsEnabled,omitempty"` // True if this service port is enabled. - Protocol string `xml:"Protocol"` // Load balancer protocol type. One of: HTTP, HTTPS, TCP. - Algorithm string `xml:"Algorithm"` // Load Balancer algorithm type. One of: IP_HASH, ROUND_ROBIN, URI, LEAST_CONN. - Port string `xml:"Port"` // Port for this service profile. - HealthCheckPort string `xml:"HealthCheckPort,omitempty"` // Health check port for this profile. - HealthCheck *LBPoolHealthCheck `xml:"HealthCheck,omitempty"` // Health check list. -} - -// LBPoolHealthCheck represents a service port health check list. -// Type: LBPoolHealthCheckType -// Namespace: http://www.vmware.com/vcloud/v1.5 -// Description: Represents a service port health check list. -// Since: 5.1 -type LBPoolHealthCheck struct { - Mode string `xml:"Mode"` // Load balancer service port health check mode. One of: TCP, HTTP, SSL. - URI string `xml:"Uri,omitempty"` // Load balancer service port health check URI. - HealthThreshold string `xml:"HealthThreshold,omitempty"` // Health threshold for this service port. - UnhealthThreshold string `xml:"UnhealthThreshold,omitempty"` // Unhealth check port for this profile. - Interval string `xml:"Interval,omitempty"` // Interval between health checks. - Timeout string `xml:"Timeout,omitempty"` // Health check timeout. -} - -// LBPoolMember represents a member in a load balancer pool. -// Type: LBPoolMemberType -// Namespace: http://www.vmware.com/vcloud/v1.5 -// Description: Represents a member in a load balancer pool. -// Since: 5.1 -type LBPoolMember struct { - IPAddress string `xml:"IpAddress"` // Ip Address for load balancer member. - Weight string `xml:"Weight"` // Weight of this member. - ServicePort *LBPoolServicePort `xml:"ServicePort,omitempty"` // Load balancer member service port. -} +// LBMonitor defines health check parameters for a particular type of network traffic +// Reference: vCloud Director API for NSX Programming Guide +// https://code.vmware.com/docs/6900/vcloud-director-api-for-nsx-programming-guide +type LBMonitor struct { + XMLName xml.Name `xml:"monitor"` + ID string `xml:"monitorId,omitempty"` + Type string `xml:"type"` + Interval int `xml:"interval,omitempty"` + Timeout int `xml:"timeout,omitempty"` + MaxRetries int `xml:"maxRetries,omitempty"` + Method string `xml:"method,omitempty"` + URL string `xml:"url,omitempty"` + Expected string `xml:"expected,omitempty"` + Name string `xml:"name,omitempty"` + Send string `xml:"send,omitempty"` + Receive string `xml:"receive,omitempty"` + Extension string `xml:"extension,omitempty"` +} + +type LBMonitors []LBMonitor // LoadBalancerVirtualServer represents a load balancer virtual server. // Type: LoadBalancerVirtualServerType