diff --git a/.gitignore b/.gitignore index af17b90f9..6572b8bec 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ website/node_modules *~ .*.swp .idea +.vscode/ *.iml *.test *.iml diff --git a/CHANGELOG.md b/CHANGELOG.md index b7d64f23d..572a289a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,12 @@ ## 2.4.0 (Unreleased) IMPROVEMENTS: -* Change resource handling to use locking mechanism when resource parallel handling is not supported by vCD. [GH-#255] +* Change resource handling to use locking mechanism when resource parallel handling is not supported by vCD. [GH-255] + +FEATURES: + +* **New Resource:** Load Balancer Service Monitor `vcd_lb_service_monitor` - [GH-256] +* **New Data Source:** Load Balancer Service Monitor `vcd_lb_service_monitor` - [GH-256] ## 2.3.0 (May 29, 2019) diff --git a/go.mod b/go.mod index f6a73c0be..91cd308f4 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,5 @@ go 1.12 require ( github.com/hashicorp/terraform v0.12.0 - github.com/vmware/go-vcloud-director/v2 v2.3.0-alpha.1 + github.com/vmware/go-vcloud-director/v2 v2.3.0-alpha.3 ) diff --git a/go.sum b/go.sum index a8bbafce5..04090d9cc 100644 --- a/go.sum +++ b/go.sum @@ -301,8 +301,8 @@ github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4A github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.1+incompatible h1:RMF1enSPeKTlXrXdOcqjFUElywVZjjC6pqse21bKbEU= github.com/vmihailenco/msgpack v4.0.1+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= -github.com/vmware/go-vcloud-director/v2 v2.3.0-alpha.1 h1:zQD5RbcompjwAeACoJjtMgTeUc4lZaLceBv66j5Ayy8= -github.com/vmware/go-vcloud-director/v2 v2.3.0-alpha.1/go.mod h1:HonlGxbjJ1NAibWh99eE4/S2l6ZOZ5KJzKK1rh2a9vc= +github.com/vmware/go-vcloud-director/v2 v2.3.0-alpha.3 h1:cgQGF5SmFtHPbwUESzlKu6C8mXBoCei/Wqkji8aScBM= +github.com/vmware/go-vcloud-director/v2 v2.3.0-alpha.3/go.mod h1:+Hq7ryFfgZqsO6mXH29RQFnpIMSujCOMI57otHoXHhQ= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= diff --git a/vcd/datasource_vcd_lb_service_monitor.go b/vcd/datasource_vcd_lb_service_monitor.go new file mode 100644 index 000000000..7c5e88f3b --- /dev/null +++ b/vcd/datasource_vcd_lb_service_monitor.go @@ -0,0 +1,92 @@ +package vcd + +import ( + "fmt" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +func datasourceVcdLbServiceMonitor() *schema.Resource { + return &schema.Resource{ + Read: datasourceVcdLbServiceMonitorRead, + Schema: map[string]*schema.Schema{ + "org": { + Type: schema.TypeString, + Optional: true, + Description: "vCD organization in which the Service Monitor is located", + }, + "vdc": { + Type: schema.TypeString, + Optional: true, + Description: "vCD virtual datacenter in which the Service Monitor is located", + }, + "edge_gateway": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: "Edge gateway name in which the Service Monitor is located", + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: "Service Monitor name", + }, + "interval": &schema.Schema{ + Type: schema.TypeInt, + Computed: true, + }, + "timeout": &schema.Schema{ + Type: schema.TypeInt, + Computed: true, + }, + "max_retries": &schema.Schema{ + Type: schema.TypeInt, + Computed: true, + }, + "type": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "expected": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "method": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "url": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "send": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "receive": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "extension": { + Type: schema.TypeMap, + Computed: true, + }, + }, + } +} + +func datasourceVcdLbServiceMonitorRead(d *schema.ResourceData, meta interface{}) error { + vcdClient := meta.(*VCDClient) + edgeGateway, err := vcdClient.GetEdgeGatewayFromResource(d) + if err != nil { + return fmt.Errorf(errorUnableToFindEdgeGateway, err) + } + + readLBMonitor, err := edgeGateway.ReadLBServiceMonitor(&types.LBMonitor{Name: d.Get("name").(string)}) + if err != nil { + return fmt.Errorf("unable to find load balancer service monitor with Name %s: %s", d.Get("name").(string), err) + } + + d.SetId(readLBMonitor.ID) + return setLBMonitorData(d, readLBMonitor) +} diff --git a/vcd/provider.go b/vcd/provider.go index 030f2a808..ee65d1212 100644 --- a/vcd/provider.go +++ b/vcd/provider.go @@ -83,25 +83,30 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ - "vcd_network": resourceVcdNetwork(), // DEPRECATED: replaced by vcd_network_routed - "vcd_network_routed": resourceVcdNetworkRouted(), - "vcd_network_direct": resourceVcdNetworkDirect(), - "vcd_network_isolated": resourceVcdNetworkIsolated(), - "vcd_vapp_network": resourceVcdVappNetwork(), - "vcd_vapp": resourceVcdVApp(), - "vcd_firewall_rules": resourceVcdFirewallRules(), - "vcd_dnat": resourceVcdDNAT(), - "vcd_snat": resourceVcdSNAT(), - "vcd_edgegateway_vpn": resourceVcdEdgeGatewayVpn(), - "vcd_vapp_vm": resourceVcdVAppVm(), - "vcd_org": resourceOrg(), - "vcd_org_vdc": resourceVcdOrgVdc(), - "vcd_catalog": resourceVcdCatalog(), - "vcd_catalog_item": resourceVcdCatalogItem(), - "vcd_catalog_media": resourceVcdCatalogMedia(), - "vcd_inserted_media": resourceVcdInsertedMedia(), - "vcd_independent_disk": resourceVcdIndependentDisk(), - "vcd_external_network": resourceVcdExternalNetwork(), + "vcd_network": resourceVcdNetwork(), // DEPRECATED: replaced by vcd_network_routed + "vcd_network_routed": resourceVcdNetworkRouted(), + "vcd_network_direct": resourceVcdNetworkDirect(), + "vcd_network_isolated": resourceVcdNetworkIsolated(), + "vcd_vapp_network": resourceVcdVappNetwork(), + "vcd_vapp": resourceVcdVApp(), + "vcd_firewall_rules": resourceVcdFirewallRules(), + "vcd_dnat": resourceVcdDNAT(), + "vcd_snat": resourceVcdSNAT(), + "vcd_edgegateway_vpn": resourceVcdEdgeGatewayVpn(), + "vcd_vapp_vm": resourceVcdVAppVm(), + "vcd_org": resourceOrg(), + "vcd_org_vdc": resourceVcdOrgVdc(), + "vcd_catalog": resourceVcdCatalog(), + "vcd_catalog_item": resourceVcdCatalogItem(), + "vcd_catalog_media": resourceVcdCatalogMedia(), + "vcd_inserted_media": resourceVcdInsertedMedia(), + "vcd_independent_disk": resourceVcdIndependentDisk(), + "vcd_external_network": resourceVcdExternalNetwork(), + "vcd_lb_service_monitor": resourceVcdLbServiceMonitor(), + }, + + DataSourcesMap: map[string]*schema.Resource{ + "vcd_lb_service_monitor": datasourceVcdLbServiceMonitor(), }, ConfigureFunc: providerConfigure, diff --git a/vcd/provider_test.go b/vcd/provider_test.go index 879b1e558..64a7c3b8b 100644 --- a/vcd/provider_test.go +++ b/vcd/provider_test.go @@ -51,3 +51,23 @@ func testAccPreCheck(t *testing.T) { func init() { testingTags["api"] = "provider_test.go" } + +// createTemporaryVCDConnection is meant to create a VCDClient to check environment before executing specific acceptance +// tests and before VCDClient is accessible. +func createTemporaryVCDConnection() *VCDClient { + config := Config{ + User: testConfig.Provider.User, + Password: testConfig.Provider.Password, + SysOrg: testConfig.Provider.SysOrg, + Org: testConfig.VCD.Org, + Vdc: testConfig.VCD.Vdc, + Href: testConfig.Provider.Url, + InsecureFlag: testConfig.Provider.AllowInsecure, + MaxRetryTimeout: testConfig.Provider.MaxRetryTimeout, + } + conn, err := config.Client() + if err != nil { + panic("unable to initialize VCD connection :" + err.Error()) + } + return conn +} diff --git a/vcd/resource_vcd_firewall_rules_test.go b/vcd/resource_vcd_firewall_rules_test.go index e64e40bab..ca7a3306b 100644 --- a/vcd/resource_vcd_firewall_rules_test.go +++ b/vcd/resource_vcd_firewall_rules_test.go @@ -83,21 +83,7 @@ func createFirewallRulesConfigs(existingRules *govcd.EdgeGateway) string { defaultAction := "drop" edgeGatewayName := testConfig.Networking.EdgeGateway if !vcdShortTest { - config := Config{ - User: testConfig.Provider.User, - Password: testConfig.Provider.Password, - SysOrg: testConfig.Provider.SysOrg, - Org: testConfig.VCD.Org, - Vdc: testConfig.VCD.Vdc, - Href: testConfig.Provider.Url, - InsecureFlag: testConfig.Provider.AllowInsecure, - MaxRetryTimeout: 140, - } - - conn, err := config.Client() - if err != nil { - panic(err) - } + conn := createTemporaryVCDConnection() if edgeGatewayName == "" { panic(fmt.Errorf("could not get an Edge Gateway. Variable networking.edgeGateway is not set")) diff --git a/vcd/resource_vcd_lb_service_monitor.go b/vcd/resource_vcd_lb_service_monitor.go new file mode 100644 index 000000000..791add988 --- /dev/null +++ b/vcd/resource_vcd_lb_service_monitor.go @@ -0,0 +1,335 @@ +package vcd + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +func resourceVcdLbServiceMonitor() *schema.Resource { + return &schema.Resource{ + Create: resourceVcdLbServiceMonitorCreate, + Read: resourceVcdLbServiceMonitorRead, + Update: resourceVcdLbServiceMonitorUpdate, + Delete: resourceVcdLbServiceMonitorDelete, + Importer: &schema.ResourceImporter{ + State: resourceVcdLbServiceMonitorImport, + }, + + Schema: map[string]*schema.Schema{ + "org": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "vdc": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "edge_gateway": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Edge gateway name", + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Unique Service Monitor name", + }, + "interval": &schema.Schema{ + Type: schema.TypeInt, + Default: 10, + Optional: true, + Description: "Interval in seconds at which a server is to be monitored", + }, + "timeout": &schema.Schema{ + Type: schema.TypeInt, + Default: 15, + Optional: true, + Description: "Maximum time in seconds within which a response from the server must be received", + }, + "max_retries": &schema.Schema{ + Type: schema.TypeInt, + Default: 3, + Optional: true, + Description: "Number of times the specified monitoring Method must fail sequentially before the server is declared down", + }, + "type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: "Way in which you want to send the health check request to the server. One of http, https, tcp, icmp, or udp", + ValidateFunc: validateCase("lower"), + }, + "expected": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Description: "String that the monitor expects to match in the status line of the http or https response (for example, HTTP/1.1)", + }, + "method": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Description: "Method to be used to detect server status. One of OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, or CONNECT", + ValidateFunc: validateCase("upper"), + }, + "url": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Description: "URL to be used in the server status request", + }, + "send": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Description: "Data to be sent", + }, + "receive": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Description: "String to be matched in the response content", + }, + "extension": { + Type: schema.TypeMap, + Optional: true, + Description: "Advanced monitor parameters as key=value pairs", + }, + }, + } +} + +// validateCase checks if a string is of caseType "upper" or "lower" +func validateCase(caseType string) schema.SchemaValidateFunc { + return func(i interface{}, k string) (s []string, es []error) { + v, ok := i.(string) + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be string", k)) + return + } + + switch caseType { + case "upper": + if strings.ToUpper(v) != v { + es = append(es, fmt.Errorf( + "expected string to be upper cased, got: %s", v)) + } + case "lower": + if strings.ToLower(v) != v { + es = append(es, fmt.Errorf( + "expected string to be lower cased, got: %s", v)) + } + default: + panic("unsupported validation type for validateCase() function") + } + return + } +} + +func resourceVcdLbServiceMonitorCreate(d *schema.ResourceData, meta interface{}) error { + vcdClient := meta.(*VCDClient) + vcdClient.lockParentEdgeGtw(d) + defer vcdClient.unLockParentEdgeGtw(d) + + edgeGateway, err := vcdClient.GetEdgeGatewayFromResource(d) + if err != nil { + return fmt.Errorf(errorUnableToFindEdgeGateway, err) + } + + lbMonitor, err := getLBMonitorType(d) + if err != nil { + return fmt.Errorf("unable to expand load balancer service monitor: %s", err) + } + + createdMonitor, err := edgeGateway.CreateLBServiceMonitor(lbMonitor) + if err != nil { + return fmt.Errorf("error creating new load balancer service monitor: %s", err) + } + + d.SetId(createdMonitor.ID) + return nil +} + +func resourceVcdLbServiceMonitorRead(d *schema.ResourceData, meta interface{}) error { + vcdClient := meta.(*VCDClient) + + edgeGateway, err := vcdClient.GetEdgeGatewayFromResource(d) + if err != nil { + return fmt.Errorf(errorUnableToFindEdgeGateway, err) + } + + readLBMonitor, err := edgeGateway.ReadLBServiceMonitor(&types.LBMonitor{ID: d.Id()}) + if err != nil { + d.SetId("") + return fmt.Errorf("unable to find load balancer service monitor with ID %s: %s", d.Id(), err) + } + + return setLBMonitorData(d, readLBMonitor) +} + +func resourceVcdLbServiceMonitorUpdate(d *schema.ResourceData, meta interface{}) error { + vcdClient := meta.(*VCDClient) + vcdClient.lockParentEdgeGtw(d) + defer vcdClient.unLockParentEdgeGtw(d) + + edgeGateway, err := vcdClient.GetEdgeGatewayFromResource(d) + if err != nil { + return fmt.Errorf(errorUnableToFindEdgeGateway, err) + } + + updateLBMonitorConfig, err := getLBMonitorType(d) + if err != nil { + return fmt.Errorf("could not expand monitor for update: %s", err) + } + + updatedLBMonitor, err := edgeGateway.UpdateLBServiceMonitor(updateLBMonitorConfig) + if err != nil { + return fmt.Errorf("unable to update load balancer service monitor with ID %s: %s", d.Id(), err) + } + + return setLBMonitorData(d, updatedLBMonitor) +} + +func resourceVcdLbServiceMonitorDelete(d *schema.ResourceData, meta interface{}) error { + vcdClient := meta.(*VCDClient) + vcdClient.lockParentEdgeGtw(d) + defer vcdClient.unLockParentEdgeGtw(d) + + edgeGateway, err := vcdClient.GetEdgeGatewayFromResource(d) + if err != nil { + return fmt.Errorf(errorUnableToFindEdgeGateway, err) + } + + err = edgeGateway.DeleteLBServiceMonitor(&types.LBMonitor{ID: d.Id()}) + if err != nil { + return fmt.Errorf("error deleting load balancer service monitor: %s", err) + } + + d.SetId("") + return nil +} + +// resourceVcdLbServiceMonitorImport expects dot formatted path to Load Balancer Service Monitor +// i.e. my-org.my-org-vdc.my-edge-gw.my-lb-service-monitor +func resourceVcdLbServiceMonitorImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + + resourceURI := strings.Split(d.Id(), ".") + if len(resourceURI) != 4 { + return nil, fmt.Errorf("resource name must be specified as org.VDC.edge-gw.lb-service-monitor") + } + orgName, vdcName, edgeName, monitorName := resourceURI[0], resourceURI[1], resourceURI[2], resourceURI[3] + + vcdClient := meta.(*VCDClient) + edgeGateway, err := vcdClient.GetEdgeGateway(orgName, vdcName, edgeName) + if err != nil { + return nil, fmt.Errorf(errorUnableToFindEdgeGateway, err) + } + + readLBMonitor, err := edgeGateway.ReadLBServiceMonitor(&types.LBMonitor{Name: monitorName}) + if err != nil { + return []*schema.ResourceData{}, fmt.Errorf("unable to find load balancer service monitor with ID %s: %s", d.Id(), err) + } + + d.Set("org", orgName) + d.Set("vdc", vdcName) + d.Set("edge_gateway", edgeName) + d.Set("name", monitorName) + + d.SetId(readLBMonitor.ID) + return []*schema.ResourceData{d}, nil +} + +// getLBMonitorType converts schema.ResourceData to *types.LBMonitor and is useful +// for creating API requests +func getLBMonitorType(d *schema.ResourceData) (*types.LBMonitor, error) { + lbMonitor := &types.LBMonitor{ + Name: d.Get("name").(string), + Interval: d.Get("interval").(int), + Timeout: d.Get("timeout").(int), + Type: d.Get("type").(string), + MaxRetries: d.Get("max_retries").(int), + Expected: d.Get("expected").(string), + Method: d.Get("method").(string), + URL: d.Get("url").(string), + Send: d.Get("send").(string), + Receive: d.Get("receive").(string), + Extension: getLBMonitorExtensionType(d), + } + + return lbMonitor, nil +} + +// getLBMonitorExtensionType expands the specified map for sending via API. It appends newline to every extension as +// per API requirement. Based on the research the underlying structure should not cause problems because duplicate keys +// are not needed and order of the keys does not matter for API. +// Example API call string for Extension field: +// delay=2 +// critical=3 +// escape +func getLBMonitorExtensionType(d *schema.ResourceData) string { + var extensionString string + extension := d.Get("extension").(map[string]interface{}) + for k, v := range extension { + if k != "" && v != "" { // When key and value are given it must look like "content-type=STRING" + extensionString += k + "=" + v.(string) + "\n" + } else { // If only key is specified it does not need equals sign. Like "no-body" extension + extensionString += k + "\n" + } + } + return extensionString +} + +// setLBMonitorData sets object state from *types.LBMonitor +func setLBMonitorData(d *schema.ResourceData, lBmonitor *types.LBMonitor) error { + d.Set("interval", lBmonitor.Interval) + d.Set("timeout", lBmonitor.Timeout) + d.Set("max_retries", lBmonitor.MaxRetries) + d.Set("type", lBmonitor.Type) + // Optional attributes may not be necessary + d.Set("method", lBmonitor.Method) + d.Set("url", lBmonitor.URL) + d.Set("send", lBmonitor.Send) + d.Set("receive", lBmonitor.Receive) + d.Set("expected", lBmonitor.Expected) + + if err := setLBMonitorExtensionData(d, lBmonitor); err != nil { + return err + } + + return nil +} + +// setLBMonitorExtensionData is responsible for parsing response extension field from API and +// store it in the map. It supports flattening `key=value` or `key` notations. Each of them must be +// separated by newline. +func setLBMonitorExtensionData(d *schema.ResourceData, lBmonitor *types.LBMonitor) error { + extensionStorage := make(map[string]string) + + if lBmonitor.Extension != "" { + kvList := strings.Split(lBmonitor.Extension, "\n") + for _, extensionLine := range kvList { + // Skip empty lines + if extensionLine == "" { + continue + } + + // When key=extensionLine format is present + if strings.Contains(extensionLine, "=") { + keyValue := strings.Split(extensionLine, "=") + if len(keyValue) != 2 { + return fmt.Errorf("unable to flatten extension field %s", extensionLine) + } + // Populate extension data with key value + extensionStorage[keyValue[0]] = keyValue[1] + // If there was no "=" sign then it means whole line is just key. Like `no-body`, `linespan` + } else { + extensionStorage[extensionLine] = "" + } + } + + } + + d.Set("extension", extensionStorage) + return nil +} diff --git a/vcd/resource_vcd_lb_service_monitor_test.go b/vcd/resource_vcd_lb_service_monitor_test.go new file mode 100644 index 000000000..a561768ef --- /dev/null +++ b/vcd/resource_vcd_lb_service_monitor_test.go @@ -0,0 +1,184 @@ +// +build gateway lb lbServiceMonitor ALL functional + +package vcd + +import ( + "fmt" + "regexp" + "strconv" + "strings" + "testing" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + + "github.com/hashicorp/terraform/terraform" + + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccVcdLbServiceMonitor(t *testing.T) { + + // String map to fill the template + var params = StringMap{ + "Org": testConfig.VCD.Org, + "Vdc": testConfig.VCD.Vdc, + "EdgeGateway": testConfig.Networking.EdgeGateway, + "ServiceMonitorName": t.Name(), + "Interval": 5, + "Timeout": 10, + "MaxRetries": 3, + "Method": "POST", + "Tags": "lb lbServiceMonitor", + } + + configText := templateFill(testAccVcdLbServiceMonitor_Basic, params) + debugPrintf("#[DEBUG] CONFIGURATION for step 0: %s", configText) + + params["FuncName"] = t.Name() + "-step1" + configTextStep1 := templateFill(testAccVcdLbServiceMonitor_Basic2, params) + debugPrintf("#[DEBUG] CONFIGURATION for step 1: %s", configTextStep1) + + if vcdShortTest { + t.Skip(acceptanceTestsSkipped) + return + } + + if !edgeGatewayIsAdvanced() { + t.Skip(t.Name() + "requires advanced edge gateway to work") + } + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckVcdLbServiceMonitorDestroy(params["ServiceMonitorName"].(string)), + Steps: []resource.TestStep{ + resource.TestStep{ + Config: configText, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("vcd_lb_service_monitor.lb-service-monitor", "name", params["ServiceMonitorName"].(string)), + resource.TestMatchResourceAttr("vcd_lb_service_monitor.lb-service-monitor", "id", regexp.MustCompile(`^monitor-\d*$`)), + resource.TestCheckResourceAttr("vcd_lb_service_monitor.lb-service-monitor", "method", params["Method"].(string)), + resource.TestCheckResourceAttr("vcd_lb_service_monitor.lb-service-monitor", "type", "http"), + resource.TestCheckResourceAttr("vcd_lb_service_monitor.lb-service-monitor", "url", "/health"), + // Data source testing - it must expose all fields which resource has + resource.TestCheckResourceAttr("data.vcd_lb_service_monitor.ds-lb-service-monitor", "interval", strconv.Itoa(params["Interval"].(int))), + resource.TestCheckResourceAttr("data.vcd_lb_service_monitor.ds-lb-service-monitor", "timeout", strconv.Itoa(params["Timeout"].(int))), + resource.TestCheckResourceAttr("data.vcd_lb_service_monitor.ds-lb-service-monitor", "max_retries", strconv.Itoa(params["MaxRetries"].(int))), + resource.TestMatchResourceAttr("data.vcd_lb_service_monitor.ds-lb-service-monitor", "id", regexp.MustCompile(`^monitor-\d*$`)), + resource.TestCheckResourceAttr("data.vcd_lb_service_monitor.ds-lb-service-monitor", "extension.content-type", "application/json"), + resource.TestCheckResourceAttr("data.vcd_lb_service_monitor.ds-lb-service-monitor", "extension.no-body", ""), + resource.TestCheckResourceAttr("data.vcd_lb_service_monitor.ds-lb-service-monitor", "method", params["Method"].(string)), + resource.TestCheckResourceAttr("data.vcd_lb_service_monitor.ds-lb-service-monitor", "type", "http"), + resource.TestCheckResourceAttr("data.vcd_lb_service_monitor.ds-lb-service-monitor", "send", "{\"key\": \"value\"}"), + resource.TestCheckResourceAttr("data.vcd_lb_service_monitor.ds-lb-service-monitor", "expected", "HTTP/1.1"), + resource.TestCheckResourceAttr("data.vcd_lb_service_monitor.ds-lb-service-monitor", "receive", "OK"), + resource.TestCheckResourceAttr("data.vcd_lb_service_monitor.ds-lb-service-monitor", "url", "/health"), + ), + }, + resource.TestStep{ + Config: configTextStep1, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("vcd_lb_service_monitor.lb-service-monitor", "name", params["ServiceMonitorName"].(string)), + resource.TestMatchResourceAttr("vcd_lb_service_monitor.lb-service-monitor", "id", regexp.MustCompile(`^monitor-\d*$`)), + resource.TestCheckResourceAttr("vcd_lb_service_monitor.lb-service-monitor", "type", "tcp"), + ), + }, + // Check that import works + resource.TestStep{ + ResourceName: "vcd_lb_service_monitor.service-monitor-import", + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: importStateIdByOrgVdcEdge(testConfig, params["ServiceMonitorName"].(string)), + }, + }, + }) +} + +func testAccCheckVcdLbServiceMonitorDestroy(serviceMonitorName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*VCDClient) + + edgeGateway, err := conn.GetEdgeGateway(testConfig.VCD.Org, testConfig.VCD.Vdc, testConfig.Networking.EdgeGateway) + if err != nil { + return fmt.Errorf(errorUnableToFindEdgeGateway, err) + } + + monitor, err := edgeGateway.ReadLBServiceMonitor(&types.LBMonitor{Name: serviceMonitorName}) + if !strings.Contains(err.Error(), "could not find load balancer") || monitor != nil { + return fmt.Errorf("load balancer service monitor was not deleted: %s", err) + } + return nil + } +} + +// importStateIdByOrgVdcEdge constructs an import path (ID in Terraform import terms) in the format of: +// organization.vdc.edge-gateway-nane.import-object-name (i.e. my-org.my-vdc.my-edge-gw.objectName) from TestConfig and +// object state. +func importStateIdByOrgVdcEdge(vcd TestConfig, objectName string) resource.ImportStateIdFunc { + return func(*terraform.State) (string, error) { + importId := testConfig.VCD.Org + "." + testConfig.VCD.Vdc + "." + testConfig.Networking.EdgeGateway + "." + objectName + if testConfig.VCD.Org == "" || testConfig.VCD.Vdc == "" || testConfig.Networking.EdgeGateway == "" || objectName == "" { + return "", fmt.Errorf("missing information to generate import path: %s", importId) + } + + return importId, nil + } +} + +// edgeGatewayIsAdvanced checks if edge gateway for testing is an advanced one +func edgeGatewayIsAdvanced() bool { + conn := createTemporaryVCDConnection() + + edgeGateway, err := conn.GetEdgeGateway(testConfig.VCD.Org, testConfig.VCD.Vdc, testConfig.Networking.EdgeGateway) + if err != nil { + panic("unable to find edge gateway") + } + + return edgeGateway.HasAdvancedNetworking() +} + +const testAccVcdLbServiceMonitor_Basic = ` +resource "vcd_lb_service_monitor" "lb-service-monitor" { + org = "{{.Org}}" + vdc = "{{.Vdc}}" + edge_gateway = "{{.EdgeGateway}}" + + name = "{{.ServiceMonitorName}}" + interval = {{.Interval}} + timeout = {{.Timeout}} + max_retries = {{.MaxRetries}} + type = "http" + method = "{{.Method}}" + send = "{\"key\": \"value\"}" + expected = "HTTP/1.1" + receive = "OK" + url = "/health" + + extension = { + "content-type" = "application/json" + "no-body" = "" + } +} + + +data "vcd_lb_service_monitor" "ds-lb-service-monitor" { + org = "{{.Org}}" + vdc = "{{.Vdc}}" + edge_gateway = "{{.EdgeGateway}}" + name = "${vcd_lb_service_monitor.lb-service-monitor.name}" +} +` + +const testAccVcdLbServiceMonitor_Basic2 = ` +resource "vcd_lb_service_monitor" "lb-service-monitor" { + org = "{{.Org}}" + vdc = "{{.Vdc}}" + edge_gateway = "{{.EdgeGateway}}" + + name = "{{.ServiceMonitorName}}" + type = "tcp" + interval = {{.Interval}} + timeout = {{.Timeout}} + max_retries = {{.MaxRetries}} +} +` diff --git a/vcd/test-templates/load-balancer1.tf b/vcd/test-templates/load-balancer1.tf new file mode 100644 index 000000000..2f0975e14 --- /dev/null +++ b/vcd/test-templates/load-balancer1.tf @@ -0,0 +1,45 @@ +# Remove the first '#' from the next two lines to enable options for terraform executable +## apply-options -parallelism=1 +## destroy-options -parallelism=1 + +# Edge gateway load balancer configuration. It will get more details, but for now is used to +# make sure that locks work fine and it does not fail + +# v2.4.0+ + +variable "service_monitor_count" { + default = 20 +} + +resource "vcd_lb_service_monitor" "test" { + count = "${var.service_monitor_count}" + + org = "{{.Org}}" + vdc = "{{.Vdc}}" + edge_gateway = "{{.EdgeGateway}}" + + name = "test-monitor-${count.index}" + interval = 5 + timeout = 5 + max_retries = 3 + type = "http" + method = "POST" + send = "{\"key\": \"value\"}" + expected = "HTTP/1.1" + receive = "OK" + + extension = { + "content-type" = "application/json" + "no-body" = "" + } +} + + +data "vcd_lb_service_monitor" "ds-lb" { + count = "${var.service_monitor_count}" + + org = "{{.Org}}" + vdc = "{{.Vdc}}" + edge_gateway = "{{.EdgeGateway}}" + name = "${vcd_lb_service_monitor.test[count.index].name}" +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/api.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/api.go index 76268796a..ae376927e 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/api.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/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,13 +326,27 @@ 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 { err := fmt.Errorf(message, "test error") - if strings.Contains(err.Error(), "%!(EXTRA") { - return false + return !strings.Contains(err.Error(), "%!(EXTRA") +} + +// combinedTaskErrorMessage is a general purpose function +// that returns the contents of the operation error and, if found, the error +// returned by the associated task +func combinedTaskErrorMessage(task *types.Task, err error) string { + extendedError := err.Error() + if task.Error != nil { + extendedError = fmt.Sprintf("operation error: %s - task error: [%d - %s] %s", + err, task.Error.MajorErrorCode, task.Error.MinorErrorCode, task.Error.Message) } - return true + return extendedError } diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/catalog.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/catalog.go index 56e199df4..d9fe804c2 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/catalog.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/catalog.go @@ -66,14 +66,21 @@ func NewAdminCatalog(client *Client) *AdminCatalog { func (catalog *Catalog) Delete(force, recursive bool) error { adminCatalogHREF := catalog.client.VCDHREF - adminCatalogHREF.Path += "/admin/catalog/" + getEntityNumericId(catalog.Catalog.ID) + catalogID, err := getBareEntityUuid(catalog.Catalog.ID) + if err != nil { + return err + } + if catalogID == "" { + return fmt.Errorf("empty ID returned for catalog ID %s", catalog.Catalog.ID) + } + adminCatalogHREF.Path += "/admin/catalog/" + catalogID req := catalog.client.NewRequest(map[string]string{ "force": strconv.FormatBool(force), "recursive": strconv.FormatBool(recursive), }, http.MethodDelete, adminCatalogHREF, nil) - _, err := checkResp(catalog.client.Http.Do(req)) + _, err = checkResp(catalog.client.Http.Do(req)) if err != nil { return fmt.Errorf("error deleting Catalog %s: %s", catalog.Catalog.ID, err) @@ -82,14 +89,6 @@ func (catalog *Catalog) Delete(force, recursive bool) error { return nil } -// slice only numeric part from ID:"urn:vcloud:catalog:97384890-180c-4563-b9b7-0dc50a2430b0" -func getEntityNumericId(catalogId string) string { - if catalogId != "" && (len(catalogId) > 19) { - return catalogId[19:] - } - return "" -} - // Deletes the Catalog, returning an error if the vCD call fails. // Link to API call: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/DELETE-Catalog.html func (adminCatalog *AdminCatalog) Delete(force, recursive bool) error { diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/edgegateway.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/edgegateway.go index 5a2fa8775..ace1efc3b 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/edgegateway.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/edgegateway.go @@ -11,6 +11,7 @@ import ( "net/http" "net/url" "regexp" + "strconv" "strings" "time" @@ -23,6 +24,12 @@ type EdgeGateway struct { client *Client } +// Simplified structure used to list networks connected to an edge gateway +type SimpleNetworkIdentifier struct { + Name string + InterfaceType string +} + var reErrorBusy = regexp.MustCompile(`is busy completing an operation.$`) func NewEdgeGateway(cli *Client) *EdgeGateway { @@ -634,3 +641,114 @@ func (eGW *EdgeGateway) RemoveIpsecVPN() (Task, error) { } return eGW.AddIpsecVPN(ipsecVPNConfig) } + +// Deletes the edge gateway, returning a task and an error with the operation result. +// https://code.vmware.com/apis/442/vcloud-director/doc/doc/operations/DELETE-EdgeGateway.html +func (egw *EdgeGateway) DeleteAsync(force bool, recursive bool) (Task, error) { + util.Logger.Printf("[TRACE] EdgeGateway.Delete - deleting edge gateway with force: %t, recursive: %t", force, recursive) + + if egw.EdgeGateway.HREF == "" { + return Task{}, fmt.Errorf("cannot delete, HREF is missing") + } + + egwUrl, err := url.ParseRequestURI(egw.EdgeGateway.HREF) + if err != nil { + return Task{}, fmt.Errorf("error parsing edge gateway url: %s", err) + } + + req := egw.client.NewRequest(map[string]string{ + "force": strconv.FormatBool(force), + "recursive": strconv.FormatBool(recursive), + }, http.MethodDelete, *egwUrl, nil) + resp, err := checkResp(egw.client.Http.Do(req)) + if err != nil { + return Task{}, fmt.Errorf("error deleting edge gateway: %s", err) + } + task := NewTask(egw.client) + if err = decodeBody(resp, task.Task); err != nil { + return Task{}, fmt.Errorf("error decoding task response: %s", err) + } + return *task, err +} + +// Deletes the edge gateway, returning an error with the operation result. +// https://code.vmware.com/apis/442/vcloud-director/doc/doc/operations/DELETE-EdgeGateway.html +func (egw *EdgeGateway) Delete(force bool, recursive bool) error { + + task, err := egw.DeleteAsync(force, recursive) + if err != nil { + return err + } + if task.Task.Status == "error" { + return fmt.Errorf(combinedTaskErrorMessage(task.Task, fmt.Errorf("edge gateway not properly destroyed"))) + } + + err = task.WaitTaskCompletion() + if err != nil { + return fmt.Errorf(combinedTaskErrorMessage(task.Task, err)) + } + + return nil +} + +// GetNetworks returns the list of networks associated with an edge gateway +// In the return structure, an interfaceType of "uplink" indicates an external network, +// while "internal" is for Org VDC routed networks +func (egw *EdgeGateway) GetNetworks() ([]SimpleNetworkIdentifier, error) { + var networks []SimpleNetworkIdentifier + err := egw.Refresh() + if err != nil { + return networks, err + } + for _, net := range egw.EdgeGateway.Configuration.GatewayInterfaces.GatewayInterface { + netIdentifier := SimpleNetworkIdentifier{ + Name: net.Name, + InterfaceType: net.InterfaceType, + } + networks = append(networks, netIdentifier) + } + + return networks, nil +} + +// HasDefaultGateway returns true if the edge gateway uses one of the external +// networks as default gateway +func (egw *EdgeGateway) HasDefaultGateway() bool { + if egw.EdgeGateway.Configuration != nil && + egw.EdgeGateway.Configuration.GatewayInterfaces != nil { + for _, gw := range egw.EdgeGateway.Configuration.GatewayInterfaces.GatewayInterface { + if gw.UseForDefaultRoute && + gw.SubnetParticipation != nil && + gw.SubnetParticipation.Gateway != "" && + gw.SubnetParticipation.Netmask != "" { + return true + } + } + } + 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/vendor/github.com/vmware/go-vcloud-director/v2/govcd/lbservicemonitor.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/lbservicemonitor.go new file mode 100644 index 000000000..cea3a4ead --- /dev/null +++ b/vendor/github.com/vmware/go-vcloud-director/v2/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/vendor/github.com/vmware/go-vcloud-director/v2/govcd/monitor.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/monitor.go index 7d7000cec..995250a9b 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/monitor.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/monitor.go @@ -117,7 +117,7 @@ func prettyDisk(disk types.Disk) string { } // Returns an External Network structure as JSON -func prettyExternalNetwork(network types.ExternalNetworkReference) string { +func prettyExternalNetwork(network types.ExternalNetwork) string { byteBuf, err := json.MarshalIndent(network, " ", " ") if err == nil { return fmt.Sprintf("%s\n", string(byteBuf)) @@ -144,12 +144,14 @@ func prettyTask(task *types.Task) string { } // Returns an Edge Gateway service configuration structure as JSON -func prettyEdgeGatewayServiceConfiguration(conf *types.EdgeGatewayServiceConfiguration) string { - byteBuf, err := json.MarshalIndent(conf, " ", " ") +//func prettyEdgeGatewayServiceConfiguration(conf types.EdgeGatewayServiceConfiguration) string { +func prettyEdgeGateway(egw types.EdgeGateway) string { + result := "" + byteBuf, err := json.MarshalIndent(egw, " ", " ") if err == nil { - return fmt.Sprintf("%s\n", string(byteBuf)) + result += fmt.Sprintf("%s\n", string(byteBuf)) } - return "" + return result } func LogNetwork(conf types.OrgVDCNetwork) { @@ -160,11 +162,11 @@ func ShowNetwork(conf types.OrgVDCNetwork) { out("screen", prettyNetworkConf(conf)) } -func LogExternalNetwork(network types.ExternalNetworkReference) { +func LogExternalNetwork(network types.ExternalNetwork) { out("log", prettyExternalNetwork(network)) } -func ShowExternalNetwork(network types.ExternalNetworkReference) { +func ShowExternalNetwork(network types.ExternalNetwork) { out("screen", prettyExternalNetwork(network)) } @@ -244,6 +246,14 @@ func outTask(destination string, task *types.Task, howManyTimes int, elapsed tim out(destination, "-------------------------------\n") } +func simpleOutTask(destination string, task *types.Task, howManyTimes int, elapsed time.Duration, first, last bool) { + if task == nil { + out(destination, "Task is null\n") + return + } + out(destination, "%s (%s) - elapsed: [%s:%d] - progress: %d%%\n", task.OperationName, task.Status, elapsed.Round(1*time.Second), howManyTimes, task.Progress) +} + func LogTask(task *types.Task, howManyTimes int, elapsed time.Duration, first, last bool) { outTask("log", task, howManyTimes, elapsed, first, last) } @@ -251,3 +261,11 @@ func LogTask(task *types.Task, howManyTimes int, elapsed time.Duration, first, l func ShowTask(task *types.Task, howManyTimes int, elapsed time.Duration, first, last bool) { outTask("screen", task, howManyTimes, elapsed, first, last) } + +func SimpleShowTask(task *types.Task, howManyTimes int, elapsed time.Duration, first, last bool) { + simpleOutTask("screen", task, howManyTimes, elapsed, first, last) +} + +func SimpleLogTask(task *types.Task, howManyTimes int, elapsed time.Duration, first, last bool) { + simpleOutTask("log", task, howManyTimes, elapsed, first, last) +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/system.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/system.go index 7f909223a..071547872 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/system.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/system.go @@ -8,11 +8,27 @@ import ( "fmt" "net/http" "net/url" + "regexp" "strings" "github.com/vmware/go-vcloud-director/v2/types/v56" ) +// Simple structure to pass Edge Gateway creation parameters. +type EdgeGatewayCreation struct { + ExternalNetworks []string // List of external networks to be linked to this gateway + DefaultGateway string // Which network should be used as default gateway (empty name = no default gateway) + OrgName string // parent Org + VdcName string // parent VDC + Name string // edge gateway name + Description string // Optional description + BackingConfiguration string // Type of backing configuration (compact, full) + AdvancedNetworkingEnabled bool // enable advanced gateway + HAEnabled bool // enable HA + UseDefaultRouteForDNSRelay bool // True if the default gateway should be used as the DNS relay + DistributedRoutingEnabled bool // If advanced networking enabled, also enable distributed routing +} + // Creates an Organization based on settings, network, and org name. // The Organization created will have these settings specified in the // settings parameter. The settings variable is defined in types.go. @@ -43,6 +59,216 @@ func CreateOrg(vcdClient *VCDClient, name string, fullName string, description s } +// Returns the UUID part of an entity ID +// From "urn:vcloud:vdc:72fefde7-4fed-45b8-a774-79b72c870325", +// will return "72fefde7-4fed-45b8-a774-79b72c870325" +// From "urn:vcloud:catalog:97384890-180c-4563-b9b7-0dc50a2430b0" +// will return "97384890-180c-4563-b9b7-0dc50a2430b0" +func getBareEntityUuid(entityId string) (string, error) { + // Regular expression to match an ID: + // 3 strings (alphanumeric + "-") separated by a colon (:) + // 1 group of 8 hexadecimal digits + // 3 groups of 4 hexadecimal + // 1 group of 12 hexadecimal digits + reGetID := regexp.MustCompile(`^[\w-]+:[\w-]+:[\w-]+:([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})$`) + matchList := reGetID.FindAllStringSubmatch(entityId, -1) + + // matchList has the format + // [][]string{[]string{"TOTAL MATCHED STRING", "CAPTURED TEXT"}} + // such as + // [][]string{[]string{"urn:vcloud:catalog:97384890-180c-4563-b9b7-0dc50a2430b0", "97384890-180c-4563-b9b7-0dc50a2430b0"}} + if len(matchList) == 0 || len(matchList[0]) < 2 { + return "", fmt.Errorf("error extracting ID from '%s'", entityId) + } + return matchList[0][1], nil +} + +// CreateEdgeGatewayAsync creates an edge gateway using a simplified configuration structure +// https://code.vmware.com/apis/442/vcloud-director/doc/doc/operations/POST-CreateEdgeGateway.html +func CreateEdgeGatewayAsync(vcdClient *VCDClient, egwc EdgeGatewayCreation) (Task, error) { + + distributed := egwc.DistributedRoutingEnabled + if !egwc.AdvancedNetworkingEnabled { + distributed = false + } + // This is the main configuration structure + egwConfiguration := &types.EdgeGateway{ + Xmlns: types.XMLNamespaceVCloud, + Name: egwc.Name, + Description: egwc.Description, + Configuration: &types.GatewayConfiguration{ + UseDefaultRouteForDNSRelay: egwc.UseDefaultRouteForDNSRelay, + HaEnabled: egwc.HAEnabled, + GatewayBackingConfig: egwc.BackingConfiguration, + AdvancedNetworkingEnabled: egwc.AdvancedNetworkingEnabled, + DistributedRoutingEnabled: distributed, + GatewayInterfaces: &types.GatewayInterfaces{ + GatewayInterface: []*types.GatewayInterface{}, + }, + EdgeGatewayServiceConfiguration: &types.GatewayFeatures{}, + }, + } + + if len(egwc.ExternalNetworks) == 0 { + return Task{}, fmt.Errorf("no external networks provided. At least one is needed") + } + + // If the user has indicated a default gateway, we make sure that it matches + // a name in the list of external networks + if egwc.DefaultGateway != "" { + defaultGatewayFound := false + for _, name := range egwc.ExternalNetworks { + if egwc.DefaultGateway == name { + defaultGatewayFound = true + } + } + if !defaultGatewayFound { + return Task{}, fmt.Errorf("default gateway (%s) selected, but its name is not among the external networks (%v)", egwc.DefaultGateway, egwc.ExternalNetworks) + } + } + // Add external networks inside the configuration structure + for _, extNetName := range egwc.ExternalNetworks { + extNet, err := GetExternalNetwork(vcdClient, extNetName) + if err != nil { + return Task{}, err + } + + // Populate the subnet participation only if default gateway was set + var subnetParticipation *types.SubnetParticipation + if egwc.DefaultGateway != "" && extNet.ExternalNetwork.Name == egwc.DefaultGateway { + for _, net := range extNet.ExternalNetwork.Configuration.IPScopes.IPScope { + if net.IsEnabled { + subnetParticipation = &types.SubnetParticipation{ + Gateway: net.Gateway, + Netmask: net.Netmask, + } + break + } + } + } + networkConf := &types.GatewayInterface{ + Name: extNet.ExternalNetwork.Name, + DisplayName: extNet.ExternalNetwork.Name, + InterfaceType: "uplink", + Network: &types.Reference{ + HREF: extNet.ExternalNetwork.HREF, + ID: extNet.ExternalNetwork.ID, + Type: "application/vnd.vmware.admin.network+xml", + Name: extNet.ExternalNetwork.Name, + }, + UseForDefaultRoute: egwc.DefaultGateway == extNet.ExternalNetwork.Name, + SubnetParticipation: subnetParticipation, + } + + egwConfiguration.Configuration.GatewayInterfaces.GatewayInterface = + append(egwConfiguration.Configuration.GatewayInterfaces.GatewayInterface, networkConf) + } + + // Once the configuration structure has been filled using the simplified data, we delegate + // the edge gateway creation to the main configuration function. + return CreateAndConfigureEdgeGatewayAsync(vcdClient, egwc.OrgName, egwc.VdcName, egwc.Name, egwConfiguration) +} + +// CreateAndConfigureEdgeGatewayAsync creates an edge gateway using a full configuration structure +func CreateAndConfigureEdgeGatewayAsync(vcdClient *VCDClient, orgName, vdcName, egwName string, egwConfiguration *types.EdgeGateway) (Task, error) { + + if egwConfiguration.Name != egwName { + return Task{}, fmt.Errorf("name mismatch: '%s' used as parameter but '%s' in the configuration structure", egwName, egwConfiguration.Name) + } + adminOrg, err := GetAdminOrgByName(vcdClient, orgName) + if err != nil { + return Task{}, err + } + vdc, err := adminOrg.GetVdcByName(vdcName) + if err != nil { + return Task{}, err + } + + egwCreateHREF := vcdClient.Client.VCDHREF + + vdcId, err := getBareEntityUuid(vdc.Vdc.ID) + if err != nil { + return Task{}, fmt.Errorf("error retrieving ID from Vdc %s: %s", vdcName, err) + } + if vdcId == "" { + return Task{}, fmt.Errorf("error retrieving ID from Vdc %s - empty ID returned", vdcName) + } + egwCreateHREF.Path += fmt.Sprintf("/admin/vdc/%s/edgeGateways", vdcId) + + // The first task is the creation task. It is quick, and does only create the vCD entity, + // but not yet deploy the underlying VM + creationTask, err := vcdClient.Client.ExecuteTaskRequest(egwCreateHREF.String(), http.MethodPost, + "application/vnd.vmware.admin.edgeGateway+xml", "error instantiating a new Edge Gateway: %s", egwConfiguration) + + if err != nil { + return Task{}, err + } + + err = creationTask.WaitTaskCompletion() + + if err != nil { + return Task{}, err + } + + // After creation, there is a build task that supervises the gateway deployment + for _, innerTask := range creationTask.Task.Tasks.Task { + if innerTask.OperationName == "networkEdgeGatewayCreate" { + deployTask := Task{ + Task: innerTask, + client: &vcdClient.Client, + } + return deployTask, nil + } + } + return Task{}, fmt.Errorf("no deployment task found for edge gateway %s - The edge gateway might have been created, but not deployed properly", egwName) +} + +// Private convenience function used by CreateAndConfigureEdgeGateway and CreateEdgeGateway to +// process the task and return the object that was created. +// It should not be invoked directly. +func createEdgeGateway(vcdClient *VCDClient, egwc EdgeGatewayCreation, egwConfiguration *types.EdgeGateway) (EdgeGateway, error) { + var task Task + var err error + if egwConfiguration != nil { + task, err = CreateAndConfigureEdgeGatewayAsync(vcdClient, egwc.OrgName, egwc.VdcName, egwc.Name, egwConfiguration) + } else { + task, err = CreateEdgeGatewayAsync(vcdClient, egwc) + } + + if err != nil { + return EdgeGateway{}, err + } + err = task.WaitTaskCompletion() + if err != nil { + return EdgeGateway{}, fmt.Errorf("%s", combinedTaskErrorMessage(task.Task, err)) + } + + // The edge gateway is created. Now we retrieve it from the server + org, err := GetAdminOrgByName(vcdClient, egwc.OrgName) + if err != nil { + return EdgeGateway{}, err + } + vdc, err := org.GetVdcByName(egwc.VdcName) + if err != nil { + return EdgeGateway{}, err + } + egw, err := vdc.FindEdgeGateway(egwc.Name) + if err != nil { + return EdgeGateway{}, err + } + return egw, nil +} + +// CreateAndConfigureEdgeGateway creates an edge gateway using a full configuration structure +func CreateAndConfigureEdgeGateway(vcdClient *VCDClient, orgName, vdcName, egwName string, egwConfiguration *types.EdgeGateway) (EdgeGateway, error) { + return createEdgeGateway(vcdClient, EdgeGatewayCreation{OrgName: orgName, VdcName: vdcName, Name: egwName}, egwConfiguration) +} + +// CreateEdgeGateway creates an edge gateway using a simplified configuration structure +func CreateEdgeGateway(vcdClient *VCDClient, egwc EdgeGatewayCreation) (EdgeGateway, error) { + return createEdgeGateway(vcdClient, egwc, nil) +} + // If user specifies a valid organization name, then this returns a // organization object. If no valid org is found, it returns an empty // org and no error. Otherwise it returns an error and an empty @@ -149,10 +375,8 @@ func QueryPortGroups(vcdCli *VCDClient, filter string) ([]*types.PortGroupRecord return results.Results.PortGroupRecord, nil } -// GetExternalNetwork returns ExternalNetwork object if user specifies a valid external network name. -// If no valid external network is found, it returns an empty -// ExternalNetwork and no error. Otherwise it returns an error and an empty -// ExternalNetwork object +// GetExternalNetwork returns an ExternalNetwork reference if user the network name matches an existing one. +// If no valid external network is found, it returns an empty ExternalNetwork reference and an error func GetExternalNetwork(vcdClient *VCDClient, networkName string) (*ExternalNetwork, error) { if !vcdClient.Client.IsSysAdmin { @@ -173,17 +397,22 @@ func GetExternalNetwork(vcdClient *VCDClient, networkName string) (*ExternalNetw externalNetwork := NewExternalNetwork(&vcdClient.Client) + found := false for _, netRef := range extNetworkRefs.ExternalNetworkReference { if netRef.Name == networkName { externalNetwork.ExternalNetwork.HREF = netRef.HREF err = externalNetwork.Refresh() + found = true if err != nil { return &ExternalNetwork{}, err } } } - return externalNetwork, nil + if found { + return externalNetwork, nil + } + return externalNetwork, fmt.Errorf("could not find external network named %s", networkName) } diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/task.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/task.go index 33fb05304..edc7e820b 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/task.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/task.go @@ -8,6 +8,7 @@ import ( "fmt" "net/http" "net/url" + "os" "strconv" "time" @@ -92,6 +93,7 @@ func (task *Task) WaitInspectTaskCompletion(inspectionFunc InspectionFunc, delay return fmt.Errorf("cannot refresh, Object is empty") } + taskMonitor := os.Getenv("GOVCD_TASK_MONITOR") howManyTimesRefreshed := 0 startTime := time.Now() for { @@ -126,6 +128,22 @@ func (task *Task) WaitInspectTaskCompletion(inspectionFunc InspectionFunc, delay return nil } + // If the environment variable "GOVCD_TASK_MONITOR" is set, its value + // will be used to choose among pre-defined InspectionFunc + if inspectionFunc == nil { + if taskMonitor != "" { + switch taskMonitor { + case "log": + inspectionFunc = LogTask // writes full task details to the log + case "show": + inspectionFunc = ShowTask // writes full task details to the screen + case "simple_log": + inspectionFunc = SimpleLogTask // writes a summary line for the task to the log + case "simple_show": + inspectionFunc = SimpleShowTask // writes a summary line for the task to the screen + } + } + } if inspectionFunc != nil { inspectionFunc(task.Task, howManyTimesRefreshed, diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vm.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vm.go index faa702023..37e357e54 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vm.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/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/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/constants.go b/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/constants.go index 53def40f7..474242b6f 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/constants.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/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/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/types.go b/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/types.go index b19fd7379..7ac2b8ae5 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/types.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/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 @@ -1486,6 +1505,7 @@ type InstantiateVAppTemplateParams struct { // Since: 5.1 type EdgeGateway struct { // Attributes + Xmlns string `xml:"xmlns,attr,omitempty"` HREF string `xml:"href,attr,omitempty"` // The URI of the entity. Type string `xml:"type,attr,omitempty"` // The MIME type of the entity. ID string `xml:"id,attr,omitempty"` // The entity identifier, expressed in URN format. The value of this attribute uniquely identifies the entity, persists for the life of the entity, and is never reused @@ -1512,6 +1532,8 @@ type GatewayConfiguration struct { GatewayInterfaces *GatewayInterfaces `xml:"GatewayInterfaces"` // List of Gateway interfaces. EdgeGatewayServiceConfiguration *GatewayFeatures `xml:"EdgeGatewayServiceConfiguration,omitempty"` // Represents Gateway Features. HaEnabled bool `xml:"HaEnabled,omitempty"` // True if this gateway is highly available. (Requires two vShield edge VMs.) + AdvancedNetworkingEnabled bool `xml:"AdvancedNetworkingEnabled,omitempty"` // True if the gateway uses advanced networking + DistributedRoutingEnabled bool `xml:"DistributedRoutingEnabled,omitempty"` // True if gateway is attached to a Distributed Logical Router UseDefaultRouteForDNSRelay bool `xml:"UseDefaultRouteForDnsRelay,omitempty"` // True if the default gateway on the external network selected for default route should be used as the DNS relay. } @@ -1575,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 } @@ -1602,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 diff --git a/vendor/modules.txt b/vendor/modules.txt index 2562fd530..23e7fea2b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -216,7 +216,7 @@ github.com/ulikunitz/xz/internal/hash # github.com/vmihailenco/msgpack v4.0.1+incompatible github.com/vmihailenco/msgpack github.com/vmihailenco/msgpack/codes -# github.com/vmware/go-vcloud-director/v2 v2.3.0-alpha.1 +# github.com/vmware/go-vcloud-director/v2 v2.3.0-alpha.3 github.com/vmware/go-vcloud-director/v2/govcd github.com/vmware/go-vcloud-director/v2/types/v56 github.com/vmware/go-vcloud-director/v2/util diff --git a/website/docs/d/lb_service_monitor.html.markdown b/website/docs/d/lb_service_monitor.html.markdown new file mode 100644 index 000000000..dad0504d4 --- /dev/null +++ b/website/docs/d/lb_service_monitor.html.markdown @@ -0,0 +1,42 @@ +--- +layout: "vcd" +page_title: "vCloudDirector: vcd_lb_service_monitor" +sidebar_current: "docs-vcd-data-source-lb-service-monitor" +description: |- + Provides a NSX load balancer service monitor data source. +--- + +# vcd\_lb\_service\_monitor + +Provides a vCloud Director Edge Gateway Load Balancer Service Monitor data source. A service monitor +defines health check parameters for a particular type of network traffic. It can be associated with +a pool. Pool members are monitored according to the service monitor parameters. + +~> **Note:** See additional support notes in [service monitor resource page](/docs/providers/vcd/r/lb_service_monitor.html). + +Supported in provider *v2.4+* + +## Example Usage + +```hcl +data "vcd_lb_service_monitor" "my-monitor" { + org = "my-org" + vdc = "my-org-vdc" + edge_gateway = "my-edge-gw" + + name = "not-managed" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `edge_gateway` - (Required) The name of the edge gateway on which the service monitor is defined +* `vdc` - (Optional) The name of VDC to use, optional if defined at provider level +* `org` - (Optional) The name of organization to use, optional if defined at provider level. Useful when connected as sysadmin working across different organisations +* `name` - (Required) Service Monitor name for identifying the exact service monitor + +## Attribute Reference + +All the attributes defined in `vcd_lb_service_monitor` resource are be available. diff --git a/website/docs/r/lb_service_monitor.html.markdown b/website/docs/r/lb_service_monitor.html.markdown new file mode 100644 index 000000000..a6c7534a6 --- /dev/null +++ b/website/docs/r/lb_service_monitor.html.markdown @@ -0,0 +1,99 @@ +--- +layout: "vcd" +page_title: "vCloudDirector: vcd_lb_service_monitor" +sidebar_current: "docs-vcd-resource-lb-service-monitor" +description: |- + Provides a NSX load balancer service monitor resource. +--- + +# vcd\_lb\_service\_monitor + +Provides a vCloud Director Edge Gateway Load Balancer Service Monitor resource. A service monitor +defines health check parameters for a particular type of network traffic. It can be associated with +a pool. Pool members are monitored according to the service monitor parameters. + +~> **Note:** To make load balancing work one must ensure that load balancing is enabled on edge gateway (edge gateway must be advanced). +This depends on NSX version to work properly. Please refer to [VMware Product Interoperability Matrices](https://www.vmware.com/resources/compatibility/sim/interop_matrix.php#interop&29=&93=) +to check supported vCloud director and NSX for vSphere configurations. + +~> **Note:** The vCloud Director API for NSX supports a subset of the operations and objects defined in the NSX vSphere +API Guide. The API supports NSX 6.2, 6.3, and 6.4. + +Supported in provider *v2.4+* + +## Example Usage + +```hcl +provider "vcd" { + user = "${var.admin_user}" + password = "${var.admin_password}" + org = "System" + url = "https://AcmeVcd/api" +} + +resource "vcd_lb_service_monitor" "monitor" { + org = "my-org" + vdc = "my-org-vdc" + edge_gateway = "my-edge-gw" + + name = "http-monitor" + interval = "5" + timeout = "20" + max_retries = "3" + type = "http" + method = "GET" + url = "/health" + send = "{\"key\": \"value\"}" + extension = { + content-type = "application/json" + linespan = "" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `edge_gateway` - (Required) The name of the edge gateway on which the service monitor is to be created +* `vdc` - (Optional) The name of VDC to use, optional if defined at provider level +* `org` - (Optional) The name of organization to use, optional if defined at provider level. Useful when connected as sysadmin working across different organisations +* `name` - (Required) Service Monitor name + +* `interval` - (Required) Interval in seconds at which a server is to be monitored using the specified Method. +* `timeout` - (Required) Maximum time in seconds within which a response from the server must be received +* `max_retries` - (Required) Number of times the specified monitoring Method must fail sequentially before the server is declared down +* `type` - (Required) Select the way in which you want to send the health check request to the server — `http`, `https`, +`tcp`, `icmp`, or `udp`. Depending on the type selected, the remaining attributes are allowed or not +* `method` - (Optional) For types `http` and `https`. Select http method to be used to detect server status. One of OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, or CONNECT +* `url` - (Optional) For types `http` and `https`. URL to be used in the server status request +* `send` - (Optional) For types `http`, `https`, and `udp`. The data to be sent. +* `expected` - (Optional) For types `http` and `https`. String that the monitor expects to match in the status line of +the HTTP or HTTPS response (for example, `HTTP/1.1`) +* `receive` - (Optional) For types `http`, `https`, and `udp`. The string to be matched in the response content. +**Note**: When `expected` is not matched, the monitor does not try to match the Receive content +* `extension` - (Required) A map of advanced monitor parameters as key=value pairs (i.e. `max-age=SECONDS`, `invert-regex`) +**Note**: When you need a value of `key` only format just set value to empty string (i.e. `linespan = ""`) + +## Attribute Reference + +The following attributes are exported on the base level of this resource: + +* `id` - The ID of the load balancer service monitor + +## Importing + +~> **Note:** The current implementation of Terraform import can only import resources into the state. It does not generate +configuration. [More information.](https://www.terraform.io/docs/import/) + +An existing load balancer service monitor can be [imported][docs-import] into this resource +via supplying the full dot separated path for load balancer service monitor. An example is below: + +[docs-import]: /docs/import/index.html + +``` +terraform import vcd_lb_service_monitor.imported my-org.my-org-vdc.my-edge-gw.my-lb-service-monitor +``` + +The above would import the service monitor named `my-lb-service-monitor` that is defined on edge gateway +`my-edge-gw` which is configured in organization named `my-org` and vDC named `my-org-vdc`. diff --git a/website/vcd.erb b/website/vcd.erb index 53d3c515a..839fb84f8 100644 --- a/website/vcd.erb +++ b/website/vcd.erb @@ -10,6 +10,15 @@ VMware vCloudDirector Provider + > + Data Sources + + + > Resources