diff --git a/acceptance/openstack/baremetal/v1/conductors_test.go b/acceptance/openstack/baremetal/v1/conductors_test.go new file mode 100644 index 0000000000..5324a7aa1c --- /dev/null +++ b/acceptance/openstack/baremetal/v1/conductors_test.go @@ -0,0 +1,42 @@ +//go:build acceptance || baremetal || conductors +// +build acceptance baremetal conductors + +package v1 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/baremetal/v1/conductors" + "github.com/gophercloud/gophercloud/pagination" + + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestConductorsListAndGet(t *testing.T) { + clients.RequireLong(t) + + client, err := clients.NewBareMetalV1Client() + th.AssertNoErr(t, err) + client.Microversion = "1.49" + + err = conductors.List(client, conductors.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + conductorList, err := conductors.ExtractConductors(page) + if err != nil { + return false, err + } + + tools.PrintResource(t, conductorList) + + if len(conductorList) > 0 { + conductor, err := conductors.Get(client, conductorList[0].Hostname).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, conductor) + } + + return true, nil + }) + th.AssertNoErr(t, err) +} diff --git a/openstack/baremetal/v1/conductors/doc.go b/openstack/baremetal/v1/conductors/doc.go new file mode 100644 index 0000000000..904910044c --- /dev/null +++ b/openstack/baremetal/v1/conductors/doc.go @@ -0,0 +1,46 @@ +/* +Package conductors provides information and interaction with the conductors API +resource in the OpenStack Bare Metal service. + +Example to List Conductors with Detail + + conductors.List(client, conductors.ListOpts{Detail: true}).EachPage(func(page pagination.Page) (bool, error) { + conductorList, err := conductors.ExtractConductors(page) + if err != nil { + return false, err + } + + for _, n := range conductorList { + // Do something + } + + return true, nil + }) + +Example to List Conductors + + listOpts := conductors.ListOpts{ + Fields: []string{"hostname"}, + } + + conductors.List(client, listOpts).EachPage(func(page pagination.Page) (bool, error) { + conductorList, err := conductors.ExtractConductors(page) + if err != nil { + return false, err + } + + for _, n := range conductorList { + // Do something + } + + return true, nil + }) + +Example to Get Conductor + + showConductor, err := conductors.Get(client, "compute2.localdomain").Extract() + if err != nil { + panic(err) + } +*/ +package conductors diff --git a/openstack/baremetal/v1/conductors/requests.go b/openstack/baremetal/v1/conductors/requests.go new file mode 100644 index 0000000000..f5bc63d63e --- /dev/null +++ b/openstack/baremetal/v1/conductors/requests.go @@ -0,0 +1,72 @@ +package conductors + +import ( + "fmt" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToConductorListQuery() (string, error) +} + +// 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 conductor attributes you want to see returned. Marker and Limit are used +// for pagination. +type ListOpts struct { + // One or more fields to be returned in the response. + Fields []string `q:"fields"` + + // Requests a page size of items. + Limit int `q:"limit"` + + // The ID of the last-seen item. + Marker string `q:"marker"` + + // Sorts the response by the requested sort direction. + SortDir string `q:"sort_dir"` + + // Sorts the response by the this attribute value. + SortKey string `q:"sort_key"` + + // Provide additional information for the BIOS Settings + Detail bool `q:"detail"` +} + +// ToConductorListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToConductorListQuery() (string, error) { + if opts.Detail == true && len(opts.Fields) > 0 { + return "", fmt.Errorf("cannot have both fields and detail options for conductors") + } + + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List makes a request against the API to list conductors accessible to you. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToConductorListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ConductorPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get requests details on a single conductor by hostname +func Get(client *gophercloud.ServiceClient, name string) (r GetResult) { + resp, err := client.Get(getURL(client, name), &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/openstack/baremetal/v1/conductors/results.go b/openstack/baremetal/v1/conductors/results.go new file mode 100644 index 0000000000..9dd4e963c3 --- /dev/null +++ b/openstack/baremetal/v1/conductors/results.go @@ -0,0 +1,93 @@ +package conductors + +import ( + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type conductorResult struct { + gophercloud.Result +} + +// Extract interprets any conductorResult as a Conductor, if possible. +func (r conductorResult) Extract() (*Conductor, error) { + var s Conductor + err := r.ExtractInto(&s) + return &s, err +} + +func (r conductorResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "") +} + +func ExtractConductorInto(r pagination.Page, v interface{}) error { + return r.(ConductorPage).Result.ExtractIntoSlicePtr(v, "conductors") +} + +// Conductor represents a conductor in the OpenStack Bare Metal API. +type Conductor struct { + // Whether or not this Conductor is alive or not + Alive bool `json:"alive"` + + // Hostname of this conductor + Hostname string `json:"hostname"` + + // Array of drivers for this conductor. + Drivers []string `json:"drivers"` + + // Conductor group for a conductor. Case-insensitive string up to 255 characters, containing a-z, 0-9, _, -, and .. + ConductorGroup string `json:"conductor_group"` + + // The UTC date and time when the resource was created, ISO 8601 format. + CreatedAt time.Time `json:"created_at"` + + // The UTC date and time when the resource was updated, ISO 8601 format. May be “null”. + UpdatedAt time.Time `json:"updated_at"` +} + +// ConductorPage abstracts the raw results of making a List() request against +// the API. As OpenStack extensions may freely alter the response bodies of +// structures returned to the client, you may only safely access the data +// provided through the ExtractConductor call. +type ConductorPage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a page contains no conductor results. +func (r ConductorPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + s, err := ExtractConductors(r) + return len(s) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the +// next page of results. +func (r ConductorPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"conductor_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractConductors interprets the results of a single page from a List() call, +// producing a slice of Conductor entities. +func ExtractConductors(r pagination.Page) ([]Conductor, error) { + var s []Conductor + err := ExtractConductorInto(r, &s) + return s, err +} + +// GetResult is the response from a Get operation. Call its Extract +// method to interpret it as a Conductor. +type GetResult struct { + conductorResult +} diff --git a/openstack/baremetal/v1/conductors/testing/doc.go b/openstack/baremetal/v1/conductors/testing/doc.go new file mode 100644 index 0000000000..9cc2466b89 --- /dev/null +++ b/openstack/baremetal/v1/conductors/testing/doc.go @@ -0,0 +1,2 @@ +// conductors unit tests +package testing diff --git a/openstack/baremetal/v1/conductors/testing/fixtures.go b/openstack/baremetal/v1/conductors/testing/fixtures.go new file mode 100644 index 0000000000..02e671aa28 --- /dev/null +++ b/openstack/baremetal/v1/conductors/testing/fixtures.go @@ -0,0 +1,181 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/gophercloud/gophercloud/openstack/baremetal/v1/conductors" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +// ConductorListBody contains the canned body of a conductor.List response, without detail. +const ConductorListBody = ` + { + "conductors": [ + { + "hostname": "compute1.localdomain", + "conductor_group": "", + "links": [ + { + "href": "http://127.0.0.1:6385/v1/conductors/compute1.localdomain", + "rel": "self" + }, + { + "href": "http://127.0.0.1:6385/conductors/compute1.localdomain", + "rel": "bookmark" + } + ], + "alive": false + }, + { + "hostname": "compute2.localdomain", + "conductor_group": "", + "links": [ + { + "href": "http://127.0.0.1:6385/v1/conductors/compute2.localdomain", + "rel": "self" + }, + { + "href": "http://127.0.0.1:6385/conductors/compute2.localdomain", + "rel": "bookmark" + } + ], + "alive": true + } + ] + } +` + +// ConductorListDetailBody contains the canned body of a conductor.ListDetail response. +const ConductorListDetailBody = ` +{ + "conductors": [ + { + "links": [ + { + "href": "http://127.0.0.1:6385/v1/conductors/compute1.localdomain", + "rel": "self" + }, + { + "href": "http://127.0.0.1:6385/conductors/compute1.localdomain", + "rel": "bookmark" + } + ], + "created_at": "2018-08-07T08:39:21+00:00", + "hostname": "compute1.localdomain", + "conductor_group": "", + "updated_at": "2018-11-30T07:07:23+00:00", + "alive": false, + "drivers": [ + "ipmi" + ] + }, + { + "links": [ + { + "href": "http://127.0.0.1:6385/v1/conductors/compute2.localdomain", + "rel": "self" + }, + { + "href": "http://127.0.0.1:6385/conductors/compute2.localdomain", + "rel": "bookmark" + } + ], + "created_at": "2018-12-05T07:03:19+00:00", + "hostname": "compute2.localdomain", + "conductor_group": "", + "updated_at": "2018-12-05T07:03:21+00:00", + "alive": true, + "drivers": [ + "ipmi" + ] + } + ] +} +` + +// SingleConductorBody is the canned body of a Get request on an existing conductor. +const SingleConductorBody = ` +{ + "links": [ + { + "href": "http://127.0.0.1:6385/v1/conductors/compute2.localdomain", + "rel": "self" + }, + { + "href": "http://127.0.0.1:6385/conductors/compute2.localdomain", + "rel": "bookmark" + } + ], + "created_at": "2018-12-05T07:03:19+00:00", + "hostname": "compute2.localdomain", + "conductor_group": "", + "updated_at": "2018-12-05T07:03:21+00:00", + "alive": true, + "drivers": [ + "ipmi" + ] +} +` + +var ( + createdAtFoo, _ = time.Parse(time.RFC3339, "2018-12-05T07:03:19+00:00") + updatedAt, _ = time.Parse(time.RFC3339, "2018-12-05T07:03:21+00:00") + + ConductorFoo = conductors.Conductor{ + CreatedAt: createdAtFoo, + UpdatedAt: updatedAt, + Hostname: "compute2.localdomain", + ConductorGroup: "", + Alive: true, + Drivers: []string{ + "ipmi", + }, + } +) + +// HandleConductorListSuccessfully sets up the test server to respond to a server List request. +func HandleConductorListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/conductors", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprintf(w, ConductorListBody) + + case "9e5476bd-a4ec-4653-93d6-72c93aa682ba": + fmt.Fprintf(w, `{ "servers": [] }`) + default: + t.Fatalf("/conductors invoked with unexpected marker=[%s]", marker) + } + }) +} + +// HandleConductorListDetailSuccessfully sets up the test server to respond to a server List request. +func HandleConductorListDetailSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/conductors", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + w.Header().Add("Content-Type", "application/json") + r.ParseForm() + + fmt.Fprintf(w, ConductorListDetailBody) + }) +} + +func HandleConductorGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/conductors/1234asdf", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + fmt.Fprintf(w, SingleConductorBody) + }) +} diff --git a/openstack/baremetal/v1/conductors/testing/requests_test.go b/openstack/baremetal/v1/conductors/testing/requests_test.go new file mode 100644 index 0000000000..b05495a5fd --- /dev/null +++ b/openstack/baremetal/v1/conductors/testing/requests_test.go @@ -0,0 +1,106 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/baremetal/v1/conductors" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestListConductors(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleConductorListSuccessfully(t) + + pages := 0 + err := conductors.List(client.ServiceClient(), conductors.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := conductors.ExtractConductors(page) + if err != nil { + return false, err + } + + if len(actual) != 2 { + t.Fatalf("Expected 2 conductors, got %d", len(actual)) + } + th.AssertEquals(t, "compute1.localdomain", actual[0].Hostname) + th.AssertEquals(t, "compute2.localdomain", actual[1].Hostname) + + return true, nil + }) + + th.AssertNoErr(t, err) + + if pages != 1 { + t.Errorf("Expected 1 page, saw %d", pages) + } +} + +func TestListDetailConductors(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleConductorListDetailSuccessfully(t) + + pages := 0 + err := conductors.List(client.ServiceClient(), conductors.ListOpts{Detail: true}).EachPage(func(page pagination.Page) (bool, error) { + pages++ + + actual, err := conductors.ExtractConductors(page) + if err != nil { + return false, err + } + + if len(actual) != 2 { + t.Fatalf("Expected 2 conductors, got %d", len(actual)) + } + th.AssertEquals(t, "compute1.localdomain", actual[0].Hostname) + th.AssertEquals(t, false, actual[0].Alive) + th.AssertEquals(t, "compute2.localdomain", actual[1].Hostname) + th.AssertEquals(t, true, actual[1].Alive) + + return true, nil + }) + + th.AssertNoErr(t, err) + + if pages != 1 { + t.Errorf("Expected 1 page, saw %d", pages) + } +} + +func TestListOpts(t *testing.T) { + // Detail cannot take Fields + optsDetail := conductors.ListOpts{ + Fields: []string{"hostname", "alive"}, + Detail: true, + } + + opts := conductors.ListOpts{ + Fields: []string{"hostname", "alive"}, + } + + _, err := optsDetail.ToConductorListQuery() + th.AssertEquals(t, err.Error(), "cannot have both fields and detail options for conductors") + + // Regular ListOpts can + query, err := opts.ToConductorListQuery() + th.AssertEquals(t, query, "?fields=hostname&fields=alive") + th.AssertNoErr(t, err) +} + +func TestGetConductor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleConductorGetSuccessfully(t) + + c := client.ServiceClient() + actual, err := conductors.Get(c, "1234asdf").Extract() + if err != nil { + t.Fatalf("Unexpected Get error: %v", err) + } + + th.CheckDeepEquals(t, ConductorFoo, *actual) +} diff --git a/openstack/baremetal/v1/conductors/urls.go b/openstack/baremetal/v1/conductors/urls.go new file mode 100644 index 0000000000..a52e1e5ca5 --- /dev/null +++ b/openstack/baremetal/v1/conductors/urls.go @@ -0,0 +1,11 @@ +package conductors + +import "github.com/gophercloud/gophercloud" + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("conductors") +} + +func getURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("conductors", id) +}