diff --git a/internal/acceptance/openstack/baremetal/httpbasic/portgroups_test.go b/internal/acceptance/openstack/baremetal/httpbasic/portgroups_test.go new file mode 100644 index 0000000000..b3ed1948e6 --- /dev/null +++ b/internal/acceptance/openstack/baremetal/httpbasic/portgroups_test.go @@ -0,0 +1,50 @@ +package httpbasic + +import ( + "context" + "testing" + + "github.com/gophercloud/gophercloud/v2/internal/acceptance/clients" + v1 "github.com/gophercloud/gophercloud/v2/internal/acceptance/openstack/baremetal/v1" + "github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/portgroups" + "github.com/gophercloud/gophercloud/v2/pagination" + th "github.com/gophercloud/gophercloud/v2/testhelper" +) + +func TestPortGroupsCreateDestroy(t *testing.T) { + clients.RequireLong(t) + clients.RequireIronicHTTPBasic(t) + + client, err := clients.NewBareMetalV1HTTPBasic() + th.AssertNoErr(t, err) + + // NOTE(sharpz7) - increased due to create fake node requiring it. + client.Microversion = "1.50" + + node, err := v1.CreateFakeNode(t, client) + th.AssertNoErr(t, err) + defer v1.DeleteNode(t, client, node) + + portgroup, err := v1.CreatePortGroup(t, client, node) + th.AssertNoErr(t, err) + defer v1.DeletePortGroup(t, client, portgroup) + + // Verify the portgroup exists by listing + err = portgroups.List(client, portgroups.ListOpts{ + Node: node.UUID, + }).EachPage(context.TODO(), func(_ context.Context, page pagination.Page) (bool, error) { + pg, err := portgroups.ExtractPortGroups(page) + if err != nil { + return false, err + } + + for _, p := range pg { + if p.UUID == portgroup.UUID { + return true, nil + } + } + + return false, nil + }) + th.AssertNoErr(t, err) +} diff --git a/internal/acceptance/openstack/baremetal/noauth/portgroups_test.go b/internal/acceptance/openstack/baremetal/noauth/portgroups_test.go new file mode 100644 index 0000000000..c4b95bba6f --- /dev/null +++ b/internal/acceptance/openstack/baremetal/noauth/portgroups_test.go @@ -0,0 +1,49 @@ +package noauth + +import ( + "context" + "testing" + + "github.com/gophercloud/gophercloud/v2/internal/acceptance/clients" + v1 "github.com/gophercloud/gophercloud/v2/internal/acceptance/openstack/baremetal/v1" + "github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/portgroups" + "github.com/gophercloud/gophercloud/v2/pagination" + th "github.com/gophercloud/gophercloud/v2/testhelper" +) + +func TestPortGroupsCreateDestroy(t *testing.T) { + clients.RequireLong(t) + + client, err := clients.NewBareMetalV1Client() + th.AssertNoErr(t, err) + + // NOTE(sharpz7) - increased due to create fake node requiring it. + client.Microversion = "1.50" + + node, err := v1.CreateFakeNode(t, client) + th.AssertNoErr(t, err) + defer v1.DeleteNode(t, client, node) + + portgroup, err := v1.CreatePortGroup(t, client, node) + th.AssertNoErr(t, err) + defer v1.DeletePortGroup(t, client, portgroup) + + // Verify the portgroup exists by listing + err = portgroups.List(client, portgroups.ListOpts{ + Node: node.UUID, + }).EachPage(context.TODO(), func(_ context.Context, page pagination.Page) (bool, error) { + pg, err := portgroups.ExtractPortGroups(page) + if err != nil { + return false, err + } + + for _, p := range pg { + if p.UUID == portgroup.UUID { + return true, nil + } + } + + return false, nil + }) + th.AssertNoErr(t, err) +} diff --git a/internal/acceptance/openstack/baremetal/v1/baremetal.go b/internal/acceptance/openstack/baremetal/v1/baremetal.go index a47f942dad..4c121d18cd 100644 --- a/internal/acceptance/openstack/baremetal/v1/baremetal.go +++ b/internal/acceptance/openstack/baremetal/v1/baremetal.go @@ -9,6 +9,7 @@ import ( "github.com/gophercloud/gophercloud/v2/internal/acceptance/tools" "github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/allocations" "github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/nodes" + "github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/portgroups" "github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/ports" ) @@ -76,6 +77,29 @@ func DeleteAllocation(t *testing.T, client *gophercloud.ServiceClient, allocatio t.Logf("Deleted allocation: %s", allocation.UUID) } +// CreatePortGroup creates an allocation +func CreatePortGroup(t *testing.T, client *gophercloud.ServiceClient, node *nodes.Node) (*portgroups.PortGroup, error) { + name := tools.RandomString("ACPTTEST", 16) + t.Logf("Attempting to create bare metal allocation: %s", name) + + allocation, err := portgroups.Create(context.TODO(), client, portgroups.CreateOpts{ + Name: name, + NodeUUID: node.UUID, + }).Extract() + + return allocation, err +} + +// DeletePortGroup deletes a bare metal portgroup via its UUID. +func DeletePortGroup(t *testing.T, client *gophercloud.ServiceClient, portgroup *portgroups.PortGroup) { + err := portgroups.Delete(context.TODO(), client, portgroup.UUID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete portgroup %s: %s", portgroup.UUID, err) + } + + t.Logf("Deleted portgroup: %s", portgroup.UUID) +} + // CreateFakeNode creates a node with fake-hardware. func CreateFakeNode(t *testing.T, client *gophercloud.ServiceClient) (*nodes.Node, error) { name := tools.RandomString("ACPTTEST", 16) diff --git a/internal/acceptance/openstack/baremetal/v1/portgroups_test.go b/internal/acceptance/openstack/baremetal/v1/portgroups_test.go new file mode 100644 index 0000000000..6f04223a02 --- /dev/null +++ b/internal/acceptance/openstack/baremetal/v1/portgroups_test.go @@ -0,0 +1,48 @@ +package v1 + +import ( + "context" + "testing" + + "github.com/gophercloud/gophercloud/v2/internal/acceptance/clients" + "github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/portgroups" + "github.com/gophercloud/gophercloud/v2/pagination" + th "github.com/gophercloud/gophercloud/v2/testhelper" +) + +func TestPortGroupsCreateDestroy(t *testing.T) { + clients.RequireLong(t) + + client, err := clients.NewBareMetalV1Client() + th.AssertNoErr(t, err) + + // NOTE(sharpz7) - increased due to create fake node requiring it. + client.Microversion = "1.50" + + node, err := CreateFakeNode(t, client) + th.AssertNoErr(t, err) + defer DeleteNode(t, client, node) + + portgroup, err := CreatePortGroup(t, client, node) + th.AssertNoErr(t, err) + defer DeletePortGroup(t, client, portgroup) + + // Verify the portgroup exists by listing + err = portgroups.List(client, portgroups.ListOpts{ + Node: node.UUID, + }).EachPage(context.TODO(), func(_ context.Context, page pagination.Page) (bool, error) { + pg, err := portgroups.ExtractPortGroups(page) + if err != nil { + return false, err + } + + for _, p := range pg { + if p.UUID == portgroup.UUID { + return true, nil + } + } + + return false, nil + }) + th.AssertNoErr(t, err) +} diff --git a/openstack/baremetal/v1/portgroups/requests.go b/openstack/baremetal/v1/portgroups/requests.go new file mode 100644 index 0000000000..9a06755323 --- /dev/null +++ b/openstack/baremetal/v1/portgroups/requests.go @@ -0,0 +1,149 @@ +package portgroups + +import ( + "context" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToPortGroupCreateMap() (map[string]any, error) +} + +// CreateOpts specifies port group creation parameters +type CreateOpts struct { + // NodeUUID is the UUID of the Node this resource belongs to + NodeUUID string `json:"node_uuid" required:"true"` + + // Address is the physical hardware address of this Portgroup, + // typically the hardware MAC address + Address string `json:"address,omitempty"` + + // Name is a human-readable identifier for the Portgroup resource + Name string `json:"name,omitempty"` + + // Mode is the mode of the port group. For possible values, refer to + // https://www.kernel.org/doc/Documentation/networking/bonding.txt + // If not specified, it will be set to the value of the + // [DEFAULT]default_portgroup_mode configuration option. + // When set, cannot be removed from the port group. + Mode string `json:"mode,omitempty"` + + // StandalonePortsSupported indicates whether ports that are members + // of this portgroup can be used as stand-alone ports + StandalonePortsSupported bool `json:"standalone_ports_supported,omitempty"` + + // Properties contains key/value properties related to the port + // group's configuration + Properties map[string]interface{} `json:"properties,omitempty"` + + // Extra is a set of one or more arbitrary metadata key and value pairs + Extra map[string]string `json:"extra,omitempty"` + + // UUID is the UUID for the resource + UUID string `json:"uuid,omitempty"` +} + +// ToPortGroupCreateMap assembles a request body based on the contents of a CreateOpts. +func (opts CreateOpts) ToPortGroupCreateMap() (map[string]any, error) { + body, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + return body, nil +} + +// Create requests a node to be created +func Create(ctx context.Context, client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + reqBody, err := opts.ToPortGroupCreateMap() + if err != nil { + r.Err = err + return + } + + resp, err := client.Post(ctx, createURL(client), reqBody, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ListOptsBuilder allows extensions to add additional parameters to the List request. +type ListOptsBuilder interface { + ToPortGroupListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing specific query parameters to the API. +type ListOpts struct { + // Node filters the list to return only Portgroups associated with this + // specific node (name or UUID) + Node string `q:"node,omitempty"` + + // Address filters the list to return only Portgroups with the specified + // physical hardware address (typically MAC) + Address string `q:"address,omitempty"` + + // Fields specifies which fields to return in the response + // For example: "uuid,name" will return only those fields + Fields []string `q:"fields,omitempty"` + + // Limit requests a page size of items. Returns a number of items up to a limit value. + // Use with marker to implement pagination. Cannot exceed max_limit set in configuration. + Limit int `q:"limit,omitempty"` + + // Marker is the ID of the last-seen item. Use with limit to implement pagination. + // Use the ID from the response as marker in subsequent limited requests. + Marker string `q:"marker,omitempty"` + + // SortDir sorts the response by the requested direction. + // Valid values are "asc" or "desc". Default is "asc". + SortDir string `q:"sort_dir,omitempty"` + + // SortKey sorts the response by this attribute value. + // Default is "id". Multiple sort key/direction pairs can be specified. + SortKey string `q:"sort_key,omitempty"` + + // Detail indicates whether to show detailed information about the resource. + // Cannot be true if Fields parameter is specified. + Detail bool `q:"detail,omitempty"` +} + +// ToPortGroupListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToPortGroupListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List makes a request against the API to list portgroups accessible to you. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToPortGroupListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return PortGroupsPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get requests the details of an portgroup by ID. +func Get(ctx context.Context, client *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := client.Get(ctx, getURL(client, id), &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete requests the deletion of an portgroup +func Delete(ctx context.Context, client *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := client.Delete(ctx, deleteURL(client, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/openstack/baremetal/v1/portgroups/results.go b/openstack/baremetal/v1/portgroups/results.go new file mode 100644 index 0000000000..1c0c14191f --- /dev/null +++ b/openstack/baremetal/v1/portgroups/results.go @@ -0,0 +1,132 @@ +package portgroups + +import ( + "time" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// ResourceLink represents a link with href and rel attributes +type ResourceLink struct { + Href string `json:"href"` + Rel string `json:"rel"` +} + +// PortGroup represents a port group in the baremetal service +// https://docs.openstack.org/api-ref/baremetal/#portgroups-portgroups +type PortGroup struct { + // Human-readable identifier for the Portgroup resource. May be undefined. + Name string `json:"name"` + + // The UUID for the resource. + UUID string `json:"uuid"` + + // Physical hardware address of this Portgroup, typically the hardware MAC address. + Address string `json:"address,omitempty"` + + // UUID of the Node this resource belongs to. + NodeUUID string `json:"node_uuid"` + + // Indicates whether ports that are members of this portgroup can be used as + // stand-alone ports. + StandalonePortsSupported bool `json:"standalone_ports_supported"` + + // Internal metadata set and stored by the Portgroup. This field is read-only. + InternalInfo map[string]any `json:"internal_info"` + + // A set of one or more arbitrary metadata key and value pairs. + Extra map[string]any `json:"extra"` + + // Mode of the port group. For possible values, refer to + // https://www.kernel.org/doc/Documentation/networking/bonding.txt + Mode string `json:"mode"` + + // Key/value properties related to the port group's configuration. + Properties map[string]any `json:"properties"` + + // 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"` + + // A list of relative links. Includes the self and bookmark links. + Links []ResourceLink `json:"links"` + + // Links to the collection of ports belonging to this portgroup. + Ports []ResourceLink `json:"ports"` +} + +type portgroupsResult struct { + gophercloud.Result +} + +func (r portgroupsResult) Extract() (*PortGroup, error) { + var s PortGroup + err := r.ExtractInto(&s) + return &s, err +} + +func (r portgroupsResult) ExtractInto(v any) error { + return r.Result.ExtractIntoStructPtr(v, "") +} + +func ExtractPortGroupsInto(r pagination.Page, v any) error { + return r.(PortGroupsPage).Result.ExtractIntoSlicePtr(v, "portgroups") +} + +// PortGroupsPage abstracts the raw results of making a List() request against +// the API. +type PortGroupsPage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a page contains no PortGroup results. +func (r PortGroupsPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + s, err := ExtractPortGroups(r) + return len(s) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the +// next page of results. +func (r PortGroupsPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"portgroups_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractPortGroups interprets the results of a single page from a List() call, +// producing a slice of PortGroup entities. +func ExtractPortGroups(r pagination.Page) ([]PortGroup, error) { + var s []PortGroup + err := ExtractPortGroupsInto(r, &s) + return s, err +} + +// GetResult is the response from a Get operation. Call its Extract +// method to interpret it as a PortGroup. +type GetResult struct { + portgroupsResult +} + +// CreateResult is the response from a Create operation. +type CreateResult struct { + portgroupsResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/openstack/baremetal/v1/portgroups/testing/fixtures.go b/openstack/baremetal/v1/portgroups/testing/fixtures.go new file mode 100644 index 0000000000..9a8773fd84 --- /dev/null +++ b/openstack/baremetal/v1/portgroups/testing/fixtures.go @@ -0,0 +1,271 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/portgroups" + th "github.com/gophercloud/gophercloud/v2/testhelper" + "github.com/gophercloud/gophercloud/v2/testhelper/client" +) + +// PortGroupsListBody is the JSON response for listing all portgroups. +var PortGroupsListBody = ` +{ + "portgroups": [ + { + "address": "00:1a:2b:3c:4d:5e", + "created_at": "2019-02-20T09:43:58Z", + "extra": { + "description": "Primary network bond", + "location": "rack-3-unit-12" + }, + "internal_info": { + "fault_count": 0, + "last_check": "2024-03-15T10:30:00Z" + }, + "links": [ + { + "href": "http://ironic.example.com/v1/portgroups/d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796", + "rel": "self" + }, + { + "href": "http://ironic.example.com/portgroups/d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796", + "rel": "bookmark" + } + ], + "mode": "active-backup", + "name": "bond0", + "node_uuid": "f9c9a846-c53f-4b17-9f0c-dd9f459d35c8", + "ports": [ + { + "href": "http://ironic.example.com/v1/portgroups/d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796/ports", + "rel": "self" + } + ], + "properties": { + "miimon": "100", + "updelay": "1000", + "downdelay": "1000", + "xmit_hash_policy": "layer2" + }, + "standalone_ports_supported": true, + "updated_at": "2019-02-20T09:43:58Z", + "uuid": "d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796" + }, + { + "address": "11:22:33:44:55:66", + "created_at": "2019-02-20T09:43:58Z", + "extra": { + "description": "Secondary bond", + "location": "rack-1-unit-4" + }, + "internal_info": { + "fault_count": 1, + "last_check": "2024-04-01T09:00:00Z" + }, + "links": [ + { + "href": "http://ironic.example.com/v1/portgroups/a1b2c3d4-e5f6-7890-1234-56789abcdef0", + "rel": "self" + }, + { + "href": "http://ironic.example.com/portgroups/a1b2c3d4-e5f6-7890-1234-56789abcdef0", + "rel": "bookmark" + } + ], + "mode": "active-backup", + "name": "bond1", + "node_uuid": "aabbcc00-1122-3344-5566-778899aabbcc", + "ports": [ + { + "href": "http://ironic.example.com/v1/portgroups/a1b2c3d4-e5f6-7890-1234-56789abcdef0/ports", + "rel": "self" + } + ], + "properties": { + "miimon": "200", + "updelay": "500", + "downdelay": "500", + "xmit_hash_policy": "layer3+4" + }, + "standalone_ports_supported": true, + "updated_at": "2019-02-20T09:43:58Z", + "uuid": "a1b2c3d4-e5f6-7890-1234-56789abcdef0" + } + ] +} +` + +// SinglePortGroupBody returns JSON for a single portgroup. +// Here we use PortGroup1 as the example. +var SinglePortGroupBody = ` +{ + "address": "00:1a:2b:3c:4d:5e", + "created_at": "2019-02-20T09:43:58Z", + "extra": { + "description": "Primary network bond", + "location": "rack-3-unit-12" + }, + "internal_info": { + "fault_count": 0, + "last_check": "2024-03-15T10:30:00Z" + }, + "links": [ + { + "href": "http://ironic.example.com/v1/portgroups/d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796", + "rel": "self" + }, + { + "href": "http://ironic.example.com/portgroups/d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796", + "rel": "bookmark" + } + ], + "mode": "active-backup", + "name": "bond0", + "node_uuid": "f9c9a846-c53f-4b17-9f0c-dd9f459d35c8", + "ports": [ + { + "href": "http://ironic.example.com/v1/portgroups/d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796/ports", + "rel": "self" + } + ], + "properties": { + "miimon": "100", + "updelay": "1000", + "downdelay": "1000", + "xmit_hash_policy": "layer2" + }, + "standalone_ports_supported": true, + "updated_at": "2019-02-20T09:43:58Z", + "uuid": "d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796" +} +` + +var ( + createdAt, _ = time.Parse(time.RFC3339, "2019-02-20T09:43:58Z") + + // PortGroup1 is the first portgroup. + PortGroup1 = portgroups.PortGroup{ + Name: "bond0", + UUID: "d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796", + Address: "00:1a:2b:3c:4d:5e", + NodeUUID: "f9c9a846-c53f-4b17-9f0c-dd9f459d35c8", + StandalonePortsSupported: true, + InternalInfo: map[string]any{ + "fault_count": float64(0), + "last_check": "2024-03-15T10:30:00Z", + }, + Extra: map[string]any{ + "description": "Primary network bond", + "location": "rack-3-unit-12", + }, + Mode: "active-backup", + Properties: map[string]any{ + "miimon": "100", + "updelay": "1000", + "downdelay": "1000", + "xmit_hash_policy": "layer2", + }, + CreatedAt: createdAt, + UpdatedAt: createdAt, + Links: []portgroups.ResourceLink{ + { + Href: "http://ironic.example.com/v1/portgroups/d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796", + Rel: "self", + }, + { + Href: "http://ironic.example.com/portgroups/d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796", + Rel: "bookmark", + }, + }, + Ports: []portgroups.ResourceLink{ + { + Href: "http://ironic.example.com/v1/portgroups/d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796/ports", + Rel: "self", + }, + }, + } +) + +// HandlePortGroupListSuccessfully sets up the test server to respond to a +// portgroup List request. +func HandlePortGroupListSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/portgroups", 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") + if err := r.ParseForm(); err != nil { + t.Errorf("Failed to parse request form: %v", err) + } + + marker := r.Form.Get("marker") + switch marker { + case "": + // Return both portgroups. + fmt.Fprintf(w, PortGroupsListBody) + case "d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796": + // No portgroups remain. + fmt.Fprintf(w, `{ "portgroups": [] }`) + default: + t.Fatalf("/portgroups invoked with unexpected marker=[%s]", marker) + } + }) +} + +// HandlePortGroupCreationSuccessfully sets up the test server to respond to a PortGroup creation request +// with a given response. +func HandlePortGroupCreationSuccessfully(t *testing.T, response string) { + th.Mux.HandleFunc("/portgroups", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestJSONRequest(t, r, `{ + "node_uuid": "f9c9a846-c53f-4b17-9f0c-dd9f459d35c8", + "address": "00:1a:2b:3c:4d:5e", + "name": "bond0", + "mode": "active-backup", + "standalone_ports_supported": true, + "properties": { + "miimon": "100", + "updelay": "1000", + "downdelay": "1000", + "xmit_hash_policy": "layer2" + }, + "extra": { + "description": "Primary network bond", + "location": "rack-3-unit-12" + } + }`) + + w.WriteHeader(http.StatusAccepted) + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, response) + }) +} + +// HandlePortGroupDeletionSuccessfully sets up the test server to respond to a +// portgroup Deletion (DELETE) request for PortGroup2. +func HandlePortGroupDeletionSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/portgroups/d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandlePortGroupGetSuccessfully sets up the test server to respond to a +// portgroup Get request for PortGroup1. +func HandlePortGroupGetSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/portgroups/d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796", + 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") + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, SinglePortGroupBody) + }) +} diff --git a/openstack/baremetal/v1/portgroups/testing/requests_test.go b/openstack/baremetal/v1/portgroups/testing/requests_test.go new file mode 100644 index 0000000000..7a818060d4 --- /dev/null +++ b/openstack/baremetal/v1/portgroups/testing/requests_test.go @@ -0,0 +1,94 @@ +package testing + +import ( + "context" + "testing" + + "github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/portgroups" + "github.com/gophercloud/gophercloud/v2/pagination" + "github.com/gophercloud/gophercloud/v2/testhelper/client" + + th "github.com/gophercloud/gophercloud/v2/testhelper" +) + +func TestListPortGroups(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandlePortGroupListSuccessfully(t) + + pages := 0 + err := portgroups.List(client.ServiceClient(), portgroups.ListOpts{}).EachPage(context.TODO(), func(_ context.Context, page pagination.Page) (bool, error) { + pages++ + + actual, err := portgroups.ExtractPortGroups(page) + if err != nil { + return false, err + } + + if len(actual) != 2 { + t.Fatalf("Expected 2 portgroups, got %d", len(actual)) + } + th.AssertEquals(t, "d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796", actual[0].UUID) + th.AssertEquals(t, "a1b2c3d4-e5f6-7890-1234-56789abcdef0", actual[1].UUID) + th.AssertEquals(t, "bond0", actual[0].Name) + th.AssertEquals(t, "bond1", actual[1].Name) + + return true, nil + }) + + th.AssertNoErr(t, err) + + if pages != 1 { + t.Errorf("Expected 1 page, saw %d", pages) + } +} + +func TestCreatePortGroup(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandlePortGroupCreationSuccessfully(t, SinglePortGroupBody) + + actual, err := portgroups.Create(context.TODO(), client.ServiceClient(), portgroups.CreateOpts{ + Name: "bond0", + NodeUUID: "f9c9a846-c53f-4b17-9f0c-dd9f459d35c8", + Address: "00:1a:2b:3c:4d:5e", + Mode: "active-backup", + StandalonePortsSupported: true, + Properties: map[string]interface{}{ + "miimon": "100", + "updelay": "1000", + "downdelay": "1000", + "xmit_hash_policy": "layer2", + }, + Extra: map[string]string{ + "description": "Primary network bond", + "location": "rack-3-unit-12", + }, + }).Extract() + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, PortGroup1, *actual) +} + +func TestDeletePortGroup(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandlePortGroupDeletionSuccessfully(t) + + res := portgroups.Delete(context.TODO(), client.ServiceClient(), "d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796") + th.AssertNoErr(t, res.Err) +} + +func TestGetPortGroup(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandlePortGroupGetSuccessfully(t) + + c := client.ServiceClient() + actual, err := portgroups.Get(context.TODO(), c, "d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796").Extract() + if err != nil { + t.Fatalf("Unexpected Get error: %v", err) + } + + th.CheckDeepEquals(t, PortGroup1, *actual) +} diff --git a/openstack/baremetal/v1/portgroups/urls.go b/openstack/baremetal/v1/portgroups/urls.go new file mode 100644 index 0000000000..3320a8e173 --- /dev/null +++ b/openstack/baremetal/v1/portgroups/urls.go @@ -0,0 +1,23 @@ +package portgroups + +import "github.com/gophercloud/gophercloud/v2" + +func createURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("portgroups") +} + +func listURL(client *gophercloud.ServiceClient) string { + return createURL(client) +} + +func resourceURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("portgroups", id) +} + +func deleteURL(client *gophercloud.ServiceClient, id string) string { + return resourceURL(client, id) +} + +func getURL(client *gophercloud.ServiceClient, id string) string { + return resourceURL(client, id) +}