From 6150ff000d7594d977f76974d564e084ba3f2fe9 Mon Sep 17 00:00:00 2001 From: Davide Agnello Date: Sat, 14 May 2016 22:13:13 -0700 Subject: [PATCH] Adding Support for LBaaS v2 - Health Monitors --- .../extensions/lbaas_v2/monitors/requests.go | 274 +++++++++++++++ .../lbaas_v2/monitors/requests_test.go | 323 ++++++++++++++++++ .../extensions/lbaas_v2/monitors/results.go | 150 ++++++++ .../v2/extensions/lbaas_v2/monitors/urls.go | 16 + 4 files changed, 763 insertions(+) create mode 100644 openstack/networking/v2/extensions/lbaas_v2/monitors/requests.go create mode 100644 openstack/networking/v2/extensions/lbaas_v2/monitors/requests_test.go create mode 100644 openstack/networking/v2/extensions/lbaas_v2/monitors/results.go create mode 100644 openstack/networking/v2/extensions/lbaas_v2/monitors/urls.go diff --git a/openstack/networking/v2/extensions/lbaas_v2/monitors/requests.go b/openstack/networking/v2/extensions/lbaas_v2/monitors/requests.go new file mode 100644 index 00000000..71d0ade3 --- /dev/null +++ b/openstack/networking/v2/extensions/lbaas_v2/monitors/requests.go @@ -0,0 +1,274 @@ +package monitors + +import ( + "fmt" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the Monitor attributes you want to see returned. SortKey allows you to +// sort by a particular Monitor attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + TenantID string `q:"tenant_id"` + Type string `q:"type"` + Delay int `q:"delay"` + Timeout int `q:"timeout"` + MaxRetries int `q:"max_retries"` + HTTPMethod string `q:"http_method"` + URLPath string `q:"url_path"` + ExpectedCodes string `q:"expected_codes"` + AdminStateUp *bool `q:"admin_state_up"` + Status string `q:"status"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// List returns a Pager which allows you to iterate over a collection of +// routers. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those routers that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return MonitorPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Constants that represent approved monitoring types. +const ( + TypePING = "PING" + TypeTCP = "TCP" + TypeHTTP = "HTTP" + TypeHTTPS = "HTTPS" +) + +var ( + errPoolIDRequired = fmt.Errorf("Pool ID to monitor is required") + errValidTypeRequired = fmt.Errorf("A valid Type is required. Supported values are PING, TCP, HTTP and HTTPS") + errDelayRequired = fmt.Errorf("Delay is required") + errTimeoutRequired = fmt.Errorf("Timeout is required") + errMaxRetriesRequired = fmt.Errorf("MaxRetries is required") + errURLPathRequired = fmt.Errorf("URL path is required") + errExpectedCodesRequired = fmt.Errorf("ExpectedCodes is required") + errDelayMustGETimeout = fmt.Errorf("Delay must be greater than or equal to timeout") +) + +// CreateOpts contains all the values needed to create a new Health Monitor. +type CreateOpts struct { + // The Pool to Monitor. + PoolID string + + // Required for admins. Indicates the owner of the Loadbalancer. + TenantID string + + // Required. The type of probe, which is PING, TCP, HTTP, or HTTPS, that is + // sent by the load balancer to verify the member state. + Type string + + // Required. The time, in seconds, between sending probes to members. + Delay int + + // Required. Maximum number of seconds for a Monitor to wait for a ping reply + // before it times out. The value must be less than the delay value. + Timeout int + + // Required. Number of permissible ping failures before changing the member's + // status to INACTIVE. Must be a number between 1 and 10. + MaxRetries int + + // Required for HTTP(S) types. URI path that will be accessed if Monitor type + // is HTTP or HTTPS. + URLPath string + + // Required for HTTP(S) types. The HTTP method used for requests by the + // Monitor. If this attribute is not specified, it defaults to "GET". + HTTPMethod string + + // Required for HTTP(S) types. Expected HTTP codes for a passing HTTP(S) + // Monitor. You can either specify a single status like "200", or a range + // like "200-202". + ExpectedCodes string + + AdminStateUp *bool +} + +// Create is an operation which provisions a new Health Monitor. There are +// different types of Monitor you can provision: PING, TCP or HTTP(S). Below +// are examples of how to create each one. +// +// Here is an example config struct to use when creating a PING or TCP Monitor: +// +// CreateOpts{Type: TypePING, Delay: 20, Timeout: 10, MaxRetries: 3} +// CreateOpts{Type: TypeTCP, Delay: 20, Timeout: 10, MaxRetries: 3} +// +// Here is an example config struct to use when creating a HTTP(S) Monitor: +// +// CreateOpts{Type: TypeHTTP, Delay: 20, Timeout: 10, MaxRetries: 3, +// HttpMethod: "HEAD", ExpectedCodes: "200". PoolID: "2c946bfc-1804-43ab-a2ff-58f6a762b505"} +// +func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult { + var res CreateResult + + // Validate inputs + allowed := map[string]bool{TypeHTTP: true, TypeHTTPS: true, TypeTCP: true, TypePING: true} + if opts.Type == "" || allowed[opts.Type] == false { + res.Err = errValidTypeRequired + } + if opts.PoolID == "" { + res.Err = errPoolIDRequired + } + if opts.Delay == 0 { + res.Err = errDelayRequired + } + if opts.Timeout == 0 { + res.Err = errTimeoutRequired + } + if opts.MaxRetries == 0 { + res.Err = errMaxRetriesRequired + } + if opts.Type == TypeHTTP || opts.Type == TypeHTTPS { + if opts.URLPath == "" { + res.Err = errURLPathRequired + } + if opts.ExpectedCodes == "" { + res.Err = errExpectedCodesRequired + } + } + if opts.Delay < opts.Timeout { + res.Err = errDelayMustGETimeout + } + if res.Err != nil { + return res + } + + type monitor struct { + Type string `json:"type"` + PoolID string `json:"pool_id"` + Delay int `json:"delay"` + Timeout int `json:"timeout"` + MaxRetries int `json:"max_retries"` + TenantID *string `json:"tenant_id,omitempty"` + URLPath *string `json:"url_path,omitempty"` + ExpectedCodes *string `json:"expected_codes,omitempty"` + HTTPMethod *string `json:"http_method,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + } + + type request struct { + Monitor monitor `json:"health_monitor"` + } + + reqBody := request{Monitor: monitor{ + Type: opts.Type, + PoolID: opts.PoolID, + Delay: opts.Delay, + Timeout: opts.Timeout, + MaxRetries: opts.MaxRetries, + TenantID: gophercloud.MaybeString(opts.TenantID), + URLPath: gophercloud.MaybeString(opts.URLPath), + ExpectedCodes: gophercloud.MaybeString(opts.ExpectedCodes), + HTTPMethod: gophercloud.MaybeString(opts.HTTPMethod), + AdminStateUp: opts.AdminStateUp, + }} + + _, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil) + return res +} + +// Get retrieves a particular Health Monitor based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) GetResult { + var res GetResult + _, res.Err = c.Get(resourceURL(c, id), &res.Body, nil) + return res +} + +// UpdateOpts contains all the values needed to update an existing Monitor. +// Attributes not listed here but appear in CreateOpts are immutable and cannot +// be updated. +type UpdateOpts struct { + // Required. The time, in seconds, between sending probes to members. + Delay int + + // Required. Maximum number of seconds for a Monitor to wait for a ping reply + // before it times out. The value must be less than the delay value. + Timeout int + + // Required. Number of permissible ping failures before changing the member's + // status to INACTIVE. Must be a number between 1 and 10. + MaxRetries int + + // Required for HTTP(S) types. URI path that will be accessed if Monitor type + // is HTTP or HTTPS. + URLPath string + + // Required for HTTP(S) types. The HTTP method used for requests by the + // Monitor. If this attribute is not specified, it defaults to "GET". + HTTPMethod string + + // Required for HTTP(S) types. Expected HTTP codes for a passing HTTP(S) + // Monitor. You can either specify a single status like "200", or a range + // like "200-202". + ExpectedCodes string + + AdminStateUp *bool +} + +// Update is an operation which modifies the attributes of the specified Monitor. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult { + var res UpdateResult + + if opts.Delay > 0 && opts.Timeout > 0 && opts.Delay < opts.Timeout { + res.Err = errDelayMustGETimeout + } + + type monitor struct { + Delay int `json:"delay"` + Timeout int `json:"timeout"` + MaxRetries int `json:"max_retries"` + URLPath *string `json:"url_path,omitempty"` + ExpectedCodes *string `json:"expected_codes,omitempty"` + HTTPMethod *string `json:"http_method,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + } + + type request struct { + Monitor monitor `json:"health_monitor"` + } + + reqBody := request{Monitor: monitor{ + Delay: opts.Delay, + Timeout: opts.Timeout, + MaxRetries: opts.MaxRetries, + URLPath: gophercloud.MaybeString(opts.URLPath), + ExpectedCodes: gophercloud.MaybeString(opts.ExpectedCodes), + HTTPMethod: gophercloud.MaybeString(opts.HTTPMethod), + AdminStateUp: opts.AdminStateUp, + }} + + _, res.Err = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + + return res +} + +// Delete will permanently delete a particular Monitor based on its unique ID. +func Delete(c *gophercloud.ServiceClient, id string) DeleteResult { + var res DeleteResult + _, res.Err = c.Delete(resourceURL(c, id), nil) + return res +} diff --git a/openstack/networking/v2/extensions/lbaas_v2/monitors/requests_test.go b/openstack/networking/v2/extensions/lbaas_v2/monitors/requests_test.go new file mode 100644 index 00000000..4e7df2a6 --- /dev/null +++ b/openstack/networking/v2/extensions/lbaas_v2/monitors/requests_test.go @@ -0,0 +1,323 @@ +package monitors + +import ( + "fmt" + "net/http" + "testing" + + fake "github.com/rackspace/gophercloud/openstack/networking/v2/common" + "github.com/rackspace/gophercloud/pagination" + th "github.com/rackspace/gophercloud/testhelper" +) + +func TestURLs(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.AssertEquals(t, th.Endpoint()+"v2.0/lbaas/health_monitors", rootURL(fake.ServiceClient())) +} + +func TestList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lbaas/health_monitors", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "health_monitors":[ + { + "admin_state_up":true, + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "delay":10, + "max_retries":1, + "timeout":1, + "type":"PING", + "pools": [{"id": "84f1b61f-58c4-45bf-a8a9-2dafb9e5214d"}], + "id":"466c8345-28d8-4f84-a246-e04380b0461d" + }, + { + "admin_state_up":true, + "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea", + "delay":5, + "expected_codes":"200", + "max_retries":2, + "http_method":"GET", + "timeout":2, + "url_path":"/", + "type":"HTTP", + "pools": [{"id": "d459f7d8-c6ee-439d-8713-d3fc08aeed8d"}], + "id":"5d4b5228-33b0-4e60-b225-9b727c1a20e7" + } + ] +} + `) + }) + + count := 0 + + List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := ExtractMonitors(page) + if err != nil { + t.Errorf("Failed to extract monitors: %v", err) + return false, err + } + + expected := []Monitor{ + { + AdminStateUp: true, + TenantID: "83657cfcdfe44cd5920adaf26c48ceea", + Delay: 10, + MaxRetries: 1, + Timeout: 1, + Type: "PING", + ID: "466c8345-28d8-4f84-a246-e04380b0461d", + Pools: []map[string]interface{}{{"id": "84f1b61f-58c4-45bf-a8a9-2dafb9e5214d"}}, + }, + { + AdminStateUp: true, + TenantID: "83657cfcdfe44cd5920adaf26c48ceea", + Delay: 5, + ExpectedCodes: "200", + MaxRetries: 2, + Timeout: 2, + URLPath: "/", + Type: "HTTP", + HTTPMethod: "GET", + ID: "5d4b5228-33b0-4e60-b225-9b727c1a20e7", + Pools: []map[string]interface{}{{"id": "d459f7d8-c6ee-439d-8713-d3fc08aeed8d"}}, + }, + } + + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestDelayMustBeGreaterOrEqualThanTimeout(t *testing.T) { + _, err := Create(fake.ServiceClient(), CreateOpts{ + Type: "HTTP", + PoolID: "d459f7d8-c6ee-439d-8713-d3fc08aeed8d", + Delay: 1, + Timeout: 10, + MaxRetries: 5, + URLPath: "/check", + ExpectedCodes: "200-299", + }).Extract() + + if err == nil { + t.Fatalf("Expected error, got none") + } + + _, err = Update(fake.ServiceClient(), "453105b9-1754-413f-aab1-55f1af620750", UpdateOpts{ + Delay: 1, + Timeout: 10, + }).Extract() + + if err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestCreate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lbaas/health_monitors", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "health_monitor":{ + "type":"HTTP", + "pool_id": "84f1b61f-58c4-45bf-a8a9-2dafb9e5214d", + "tenant_id":"453105b9-1754-413f-aab1-55f1af620750", + "delay":20, + "timeout":10, + "max_retries":5, + "url_path":"/check", + "expected_codes":"200-299" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, ` +{ + "health_monitor":{ + "id":"f3eeab00-8367-4524-b662-55e64d4cacb5", + "tenant_id":"453105b9-1754-413f-aab1-55f1af620750", + "type":"HTTP", + "pool_id": "84f1b61f-58c4-45bf-a8a9-2dafb9e5214d", + "delay":20, + "timeout":10, + "max_retries":5, + "http_method":"GET", + "url_path":"/check", + "expected_codes":"200-299", + "admin_state_up":true, + "status":"ACTIVE" + } +} + `) + }) + + _, err := Create(fake.ServiceClient(), CreateOpts{ + Type: "HTTP", + PoolID: "84f1b61f-58c4-45bf-a8a9-2dafb9e5214d", + TenantID: "453105b9-1754-413f-aab1-55f1af620750", + Delay: 20, + Timeout: 10, + MaxRetries: 5, + URLPath: "/check", + ExpectedCodes: "200-299", + }).Extract() + + th.AssertNoErr(t, err) +} + +func TestRequiredCreateOpts(t *testing.T) { + res := Create(fake.ServiceClient(), CreateOpts{}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } + res = Create(fake.ServiceClient(), CreateOpts{Type: TypeHTTP}) + if res.Err == nil { + t.Fatalf("Expected error, got none") + } +} + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lbaas/health_monitors/f3eeab00-8367-4524-b662-55e64d4cacb5", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, ` +{ + "health_monitor":{ + "id":"f3eeab00-8367-4524-b662-55e64d4cacb5", + "tenant_id":"453105b9-1754-413f-aab1-55f1af620750", + "type":"HTTP", + "pools": [{"id": "84f1b61f-58c4-45bf-a8a9-2dafb9e5214d"}], + "delay":20, + "timeout":10, + "max_retries":5, + "http_method":"GET", + "url_path":"/check", + "expected_codes":"200-299", + "admin_state_up":true, + "status":"ACTIVE" + } +} + `) + }) + + hm, err := Get(fake.ServiceClient(), "f3eeab00-8367-4524-b662-55e64d4cacb5").Extract() + th.AssertNoErr(t, err) + + th.AssertEquals(t, "f3eeab00-8367-4524-b662-55e64d4cacb5", hm.ID) + th.AssertEquals(t, "453105b9-1754-413f-aab1-55f1af620750", hm.TenantID) + th.AssertEquals(t, "HTTP", hm.Type) + th.AssertEquals(t, "84f1b61f-58c4-45bf-a8a9-2dafb9e5214d", hm.Pools[0]["id"]) + th.AssertEquals(t, 20, hm.Delay) + th.AssertEquals(t, 10, hm.Timeout) + th.AssertEquals(t, 5, hm.MaxRetries) + th.AssertEquals(t, "GET", hm.HTTPMethod) + th.AssertEquals(t, "/check", hm.URLPath) + th.AssertEquals(t, "200-299", hm.ExpectedCodes) + th.AssertEquals(t, true, hm.AdminStateUp) + th.AssertEquals(t, "ACTIVE", hm.Status) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lbaas/health_monitors/b05e44b5-81f9-4551-b474-711a722698f7", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "health_monitor":{ + "delay": 3, + "timeout": 20, + "max_retries": 10, + "url_path": "/another_check", + "expected_codes": "301" + } +} + `) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + fmt.Fprintf(w, ` +{ + "health_monitor": { + "admin_state_up": true, + "tenant_id": "4fd44f30292945e481c7b8a0c8908869", + "delay": 3, + "max_retries": 10, + "http_method": "GET", + "timeout": 20, + "pools": [ + { + "status": "PENDING_CREATE", + "status_description": null, + "pool_id": "6e55751f-6ad4-4e53-b8d4-02e442cd21df" + } + ], + "type": "PING", + "pool_id": "84f1b61f-58c4-45bf-a8a9-2dafb9e5214d", + "id": "b05e44b5-81f9-4551-b474-711a722698f7" + } +} + `) + }) + + _, err := Update(fake.ServiceClient(), "b05e44b5-81f9-4551-b474-711a722698f7", UpdateOpts{ + Delay: 3, + Timeout: 20, + MaxRetries: 10, + URLPath: "/another_check", + ExpectedCodes: "301", + }).Extract() + + th.AssertNoErr(t, err) +} + +func TestDelete(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/lbaas/health_monitors/b05e44b5-81f9-4551-b474-711a722698f7", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusNoContent) + }) + + res := Delete(fake.ServiceClient(), "b05e44b5-81f9-4551-b474-711a722698f7") + th.AssertNoErr(t, res.Err) +} diff --git a/openstack/networking/v2/extensions/lbaas_v2/monitors/results.go b/openstack/networking/v2/extensions/lbaas_v2/monitors/results.go new file mode 100644 index 00000000..599c1308 --- /dev/null +++ b/openstack/networking/v2/extensions/lbaas_v2/monitors/results.go @@ -0,0 +1,150 @@ +package monitors + +import ( + "github.com/mitchellh/mapstructure" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/pagination" +) + +// Monitor represents a load balancer health monitor. A health monitor is used +// to determine whether or not back-end members of the VIP's pool are usable +// for processing a request. A pool can have several health monitors associated +// with it. There are different types of health monitors supported: +// +// PING: used to ping the members using ICMP. +// TCP: used to connect to the members using TCP. +// HTTP: used to send an HTTP request to the member. +// HTTPS: used to send a secure HTTP request to the member. +// +// When a pool has several monitors associated with it, each member of the pool +// is monitored by all these monitors. If any monitor declares the member as +// unhealthy, then the member status is changed to INACTIVE and the member +// won't participate in its pool's load balancing. In other words, ALL monitors +// must declare the member to be healthy for it to stay ACTIVE. +type Monitor struct { + // The unique ID for the VIP. + ID string + + // Owner of the VIP. Only an administrative user can specify a tenant ID + // other than its own. + TenantID string `json:"tenant_id" mapstructure:"tenant_id"` + + // The type of probe sent by the load balancer to verify the member state, + // which is PING, TCP, HTTP, or HTTPS. + Type string + + // The time, in seconds, between sending probes to members. + Delay int + + // The maximum number of seconds for a monitor to wait for a connection to be + // established before it times out. This value must be less than the delay value. + Timeout int + + // Number of allowed connection failures before changing the status of the + // member to INACTIVE. A valid value is from 1 to 10. + MaxRetries int `json:"max_retries" mapstructure:"max_retries"` + + // The HTTP method that the monitor uses for requests. + HTTPMethod string `json:"http_method" mapstructure:"http_method"` + + // The HTTP path of the request sent by the monitor to test the health of a + // member. Must be a string beginning with a forward slash (/). + URLPath string `json:"url_path" mapstructure:"url_path"` + + // Expected HTTP codes for a passing HTTP(S) monitor. + ExpectedCodes string `json:"expected_codes" mapstructure:"expected_codes"` + + // The administrative state of the health monitor, which is up (true) or down (false). + AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"` + + // The status of the health monitor. Indicates whether the health monitor is + // operational. + Status string + + // List of pools that are associated with the health monitor. + Pools []map[string]interface{} `mapstructure:"pools" json:"pools"` +} + +// MonitorPage is the page returned by a pager when traversing over a +// collection of health monitors. +type MonitorPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of monitors has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (p MonitorPage) NextPageURL() (string, error) { + type resp struct { + Links []gophercloud.Link `mapstructure:"health_monitors_links"` + } + + var r resp + err := mapstructure.Decode(p.Body, &r) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(r.Links) +} + +// IsEmpty checks whether a PoolPage struct is empty. +func (p MonitorPage) IsEmpty() (bool, error) { + is, err := ExtractMonitors(p) + if err != nil { + return true, nil + } + return len(is) == 0, nil +} + +// ExtractMonitors accepts a Page struct, specifically a MonitorPage struct, +// and extracts the elements into a slice of Monitor structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractMonitors(page pagination.Page) ([]Monitor, error) { + var resp struct { + Monitors []Monitor `mapstructure:"health_monitors" json:"health_monitors"` + } + + err := mapstructure.Decode(page.(MonitorPage).Body, &resp) + + return resp.Monitors, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a monitor. +func (r commonResult) Extract() (*Monitor, error) { + if r.Err != nil { + return nil, r.Err + } + + var res struct { + Monitor *Monitor `json:"health_monitor" mapstructure:"health_monitor"` + } + + err := mapstructure.Decode(r.Body, &res) + + return res.Monitor, err +} + +// CreateResult represents the result of a create operation. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/openstack/networking/v2/extensions/lbaas_v2/monitors/urls.go b/openstack/networking/v2/extensions/lbaas_v2/monitors/urls.go new file mode 100644 index 00000000..5446a2d2 --- /dev/null +++ b/openstack/networking/v2/extensions/lbaas_v2/monitors/urls.go @@ -0,0 +1,16 @@ +package monitors + +import "github.com/rackspace/gophercloud" + +const ( + rootPath = "lbaas" + resourcePath = "health_monitors" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +}