Skip to content

Commit d9b9689

Browse files
authored
Merge pull request #3334 from gophercloud/bp-v2-c6782b0-1e7e3bc
[v2] Added support for VIF's in Baremetal
2 parents b4af4f3 + cf7f9d9 commit d9b9689

6 files changed

Lines changed: 306 additions & 0 deletions

File tree

internal/acceptance/openstack/baremetal/v1/nodes_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,3 +272,51 @@ func TestNodesServicingHold(t *testing.T) {
272272
}, nodes.Active)
273273
th.AssertNoErr(t, err)
274274
}
275+
276+
func TestNodesVirtualInterfaces(t *testing.T) {
277+
clients.SkipReleasesBelow(t, "stable/2023.2") // Adjust based on when this feature was added
278+
clients.RequireLong(t)
279+
280+
client, err := clients.NewBareMetalV1Client()
281+
th.AssertNoErr(t, err)
282+
// VIFs were added in API version 1.28, but at least 1.38 is needed for tests to pass
283+
client.Microversion = "1.38"
284+
285+
node, err := CreateNode(t, client)
286+
th.AssertNoErr(t, err)
287+
defer DeleteNode(t, client, node)
288+
289+
// First, list VIFs (should be empty initially)
290+
vifs, err := nodes.ListVirtualInterfaces(context.TODO(), client, node.UUID).Extract()
291+
th.AssertNoErr(t, err)
292+
th.AssertEquals(t, 0, len(vifs))
293+
294+
// For a real test, we would need a valid VIF ID from the networking service
295+
// Since this is difficult in a test environment, we can test the API call
296+
// with a fake ID and expect it to fail with a specific error
297+
fakeVifID := "1974dcfa-836f-41b2-b541-686c100900e5"
298+
299+
// Try to attach a VIF (this will likely fail with a 404 Not Found since the VIF doesn't exist)
300+
err = nodes.AttachVirtualInterface(context.TODO(), client, node.UUID, nodes.VirtualInterfaceOpts{
301+
ID: fakeVifID,
302+
}).ExtractErr()
303+
304+
// We expect this to fail, but we're testing the API call itself
305+
// In a real environment with valid VIFs, you would check for success instead
306+
if err == nil {
307+
t.Logf("Warning: Expected error when attaching non-existent VIF, but got success. This might indicate the test environment has a VIF with ID %s", fakeVifID)
308+
}
309+
310+
// Try to detach a VIF (this will likely fail with a 404 Not Found)
311+
err = nodes.DetachVirtualInterface(context.TODO(), client, node.UUID, fakeVifID).ExtractErr()
312+
313+
// Again, we expect this to fail in most test environments
314+
if err == nil {
315+
t.Logf("Warning: Expected error when detaching non-existent VIF, but got success. This might indicate the test environment has a VIF with ID %s", fakeVifID)
316+
}
317+
318+
// List VIFs again to confirm state hasn't changed
319+
vifs, err = nodes.ListVirtualInterfaces(context.TODO(), client, node.UUID).Extract()
320+
th.AssertNoErr(t, err)
321+
th.AssertEquals(t, 0, len(vifs))
322+
}

openstack/baremetal/v1/nodes/requests.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,3 +1002,61 @@ func DetachVirtualMedia(ctx context.Context, client *gophercloud.ServiceClient,
10021002
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
10031003
return
10041004
}
1005+
1006+
// VirtualInterfaceOpts defines options for attaching a VIF to a node
1007+
type VirtualInterfaceOpts struct {
1008+
// The UUID or name of the VIF
1009+
ID string `json:"id" required:"true"`
1010+
// The UUID of a port to attach the VIF to. Cannot be specified with PortgroupUUID
1011+
PortUUID string `json:"port_uuid,omitempty"`
1012+
// The UUID of a portgroup to attach the VIF to. Cannot be specified with PortUUID
1013+
PortgroupUUID string `json:"portgroup_uuid,omitempty"`
1014+
}
1015+
1016+
// VirtualInterfaceOptsBuilder allows extensions to add additional parameters to the
1017+
// AttachVirtualInterface request.
1018+
type VirtualInterfaceOptsBuilder interface {
1019+
ToVirtualInterfaceMap() (map[string]any, error)
1020+
}
1021+
1022+
// ToVirtualInterfaceMap assembles a request body based on the contents of a VirtualInterfaceOpts.
1023+
func (opts VirtualInterfaceOpts) ToVirtualInterfaceMap() (map[string]any, error) {
1024+
if opts.PortUUID != "" && opts.PortgroupUUID != "" {
1025+
return nil, fmt.Errorf("cannot specify both port_uuid and portgroup_uuid")
1026+
}
1027+
1028+
return gophercloud.BuildRequestBody(opts, "")
1029+
}
1030+
1031+
// ListVirtualInterfaces returns a list of VIFs that are attached to the node.
1032+
func ListVirtualInterfaces(ctx context.Context, client *gophercloud.ServiceClient, id string) (r ListVirtualInterfacesResult) {
1033+
resp, err := client.Get(ctx, virtualInterfaceURL(client, id), &r.Body, &gophercloud.RequestOpts{
1034+
OkCodes: []int{200},
1035+
})
1036+
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
1037+
return
1038+
}
1039+
1040+
// AttachVirtualInterface attaches a VIF to a node.
1041+
func AttachVirtualInterface(ctx context.Context, client *gophercloud.ServiceClient, id string, opts VirtualInterfaceOptsBuilder) (r VirtualInterfaceAttachResult) {
1042+
reqBody, err := opts.ToVirtualInterfaceMap()
1043+
if err != nil {
1044+
r.Err = err
1045+
return
1046+
}
1047+
1048+
resp, err := client.Post(ctx, virtualInterfaceURL(client, id), reqBody, nil, &gophercloud.RequestOpts{
1049+
OkCodes: []int{204},
1050+
})
1051+
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
1052+
return
1053+
}
1054+
1055+
// DetachVirtualInterface detaches a VIF from a node.
1056+
func DetachVirtualInterface(ctx context.Context, client *gophercloud.ServiceClient, id string, vifID string) (r VirtualInterfaceDetachResult) {
1057+
resp, err := client.Delete(ctx, virtualInterfaceDeleteURL(client, id, vifID), &gophercloud.RequestOpts{
1058+
OkCodes: []int{204},
1059+
})
1060+
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
1061+
return
1062+
}

openstack/baremetal/v1/nodes/results.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,3 +710,40 @@ type VirtualMediaAttachResult struct {
710710
type VirtualMediaDetachResult struct {
711711
gophercloud.ErrResult
712712
}
713+
714+
// VirtualInterfaceAttachResult is the response from an AttachVirtualInterface operation.
715+
type VirtualInterfaceAttachResult struct {
716+
gophercloud.ErrResult
717+
}
718+
719+
// VirtualInterfaceDetachResult is the response from a DetachVirtualInterface operation.
720+
type VirtualInterfaceDetachResult struct {
721+
gophercloud.ErrResult
722+
}
723+
724+
// VIF represents a virtual interface attached to a node.
725+
type VIF struct {
726+
// The UUID or name of the VIF
727+
ID string `json:"id"`
728+
}
729+
730+
// ListVirtualInterfacesResult is the response from a ListVirtualInterfaces operation.
731+
type ListVirtualInterfacesResult struct {
732+
gophercloud.Result
733+
gophercloud.HeaderResult
734+
}
735+
736+
// Extract interprets any ListVirtualInterfacesResult as a list of VIFs.
737+
func (r ListVirtualInterfacesResult) Extract() ([]VIF, error) {
738+
var s struct {
739+
VIFs []VIF `json:"vifs"`
740+
}
741+
742+
err := r.Result.ExtractInto(&s)
743+
return s.VIFs, err
744+
}
745+
746+
// ExtractHeader interprets any ListVirtualInterfacesResult as a HeaderResult.
747+
func (r ListVirtualInterfacesResult) ExtractHeader() (gophercloud.HeaderResult, error) {
748+
return r.HeaderResult, nil
749+
}

openstack/baremetal/v1/nodes/testing/fixtures_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1849,3 +1849,78 @@ func HandleDetachVirtualMediaSuccessfully(t *testing.T, withType bool) {
18491849
w.WriteHeader(http.StatusNoContent)
18501850
})
18511851
}
1852+
1853+
// HandleListVirtualInterfacesSuccessfully sets up the test server to respond to a ListVirtualInterfaces request
1854+
func HandleListVirtualInterfacesSuccessfully(t *testing.T) {
1855+
th.Mux.HandleFunc("/nodes/1234asdf/vifs",
1856+
func(w http.ResponseWriter, r *http.Request) {
1857+
th.TestMethod(t, r, "GET")
1858+
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
1859+
th.TestHeader(t, r, "Accept", "application/json")
1860+
1861+
w.Header().Add("Content-Type", "application/json")
1862+
w.WriteHeader(http.StatusOK)
1863+
fmt.Fprintf(w, `
1864+
{
1865+
"vifs": [
1866+
{
1867+
"id": "1974dcfa-836f-41b2-b541-686c100900e5"
1868+
}
1869+
]
1870+
}`)
1871+
})
1872+
}
1873+
1874+
// HandleAttachVirtualInterfaceSuccessfully sets up the test server to respond to an AttachVirtualInterface request
1875+
func HandleAttachVirtualInterfaceSuccessfully(t *testing.T) {
1876+
th.Mux.HandleFunc("/nodes/1234asdf/vifs",
1877+
func(w http.ResponseWriter, r *http.Request) {
1878+
th.TestMethod(t, r, "POST")
1879+
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
1880+
th.TestHeader(t, r, "Content-Type", "application/json")
1881+
th.TestHeader(t, r, "Accept", "application/json")
1882+
th.TestJSONRequest(t, r, `{"id":"1974dcfa-836f-41b2-b541-686c100900e5"}`)
1883+
1884+
w.WriteHeader(http.StatusNoContent)
1885+
})
1886+
}
1887+
1888+
// HandleAttachVirtualInterfaceWithPortSuccessfully sets up the test server to respond to an AttachVirtualInterface request with port
1889+
func HandleAttachVirtualInterfaceWithPortSuccessfully(t *testing.T) {
1890+
th.Mux.HandleFunc("/nodes/1234asdf/vifs",
1891+
func(w http.ResponseWriter, r *http.Request) {
1892+
th.TestMethod(t, r, "POST")
1893+
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
1894+
th.TestHeader(t, r, "Content-Type", "application/json")
1895+
th.TestHeader(t, r, "Accept", "application/json")
1896+
th.TestJSONRequest(t, r, `{"id":"1974dcfa-836f-41b2-b541-686c100900e5","port_uuid":"b2f96298-5172-45e9-b174-8d1ba936ab47"}`)
1897+
1898+
w.WriteHeader(http.StatusNoContent)
1899+
})
1900+
}
1901+
1902+
// HandleAttachVirtualInterfaceWithPortgroupSuccessfully sets up the test server to respond to an AttachVirtualInterface request with portgroup
1903+
func HandleAttachVirtualInterfaceWithPortgroupSuccessfully(t *testing.T) {
1904+
th.Mux.HandleFunc("/nodes/1234asdf/vifs",
1905+
func(w http.ResponseWriter, r *http.Request) {
1906+
th.TestMethod(t, r, "POST")
1907+
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
1908+
th.TestHeader(t, r, "Content-Type", "application/json")
1909+
th.TestHeader(t, r, "Accept", "application/json")
1910+
th.TestJSONRequest(t, r, `{"id":"1974dcfa-836f-41b2-b541-686c100900e5","portgroup_uuid":"c24944b5-a52e-4c5c-9c0a-52a0235a08a2"}`)
1911+
1912+
w.WriteHeader(http.StatusNoContent)
1913+
})
1914+
}
1915+
1916+
// HandleDetachVirtualInterfaceSuccessfully sets up the test server to respond to a DetachVirtualInterface request
1917+
func HandleDetachVirtualInterfaceSuccessfully(t *testing.T) {
1918+
th.Mux.HandleFunc("/nodes/1234asdf/vifs/1974dcfa-836f-41b2-b541-686c100900e5",
1919+
func(w http.ResponseWriter, r *http.Request) {
1920+
th.TestMethod(t, r, "DELETE")
1921+
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
1922+
th.TestHeader(t, r, "Accept", "application/json")
1923+
1924+
w.WriteHeader(http.StatusNoContent)
1925+
})
1926+
}

openstack/baremetal/v1/nodes/testing/requests_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -829,3 +829,83 @@ func TestVirtualMediaDetachWithTypes(t *testing.T) {
829829
err := nodes.DetachVirtualMedia(context.TODO(), c, "1234asdf", opts).ExtractErr()
830830
th.AssertNoErr(t, err)
831831
}
832+
833+
func TestListVirtualInterfaces(t *testing.T) {
834+
th.SetupHTTP()
835+
defer th.TeardownHTTP()
836+
HandleListVirtualInterfacesSuccessfully(t)
837+
838+
c := client.ServiceClient()
839+
actual, err := nodes.ListVirtualInterfaces(context.TODO(), c, "1234asdf").Extract()
840+
th.AssertNoErr(t, err)
841+
842+
expected := []nodes.VIF{
843+
{
844+
ID: "1974dcfa-836f-41b2-b541-686c100900e5",
845+
},
846+
}
847+
848+
th.CheckDeepEquals(t, expected, actual)
849+
}
850+
851+
func TestAttachVirtualInterface(t *testing.T) {
852+
th.SetupHTTP()
853+
defer th.TeardownHTTP()
854+
HandleAttachVirtualInterfaceSuccessfully(t)
855+
856+
c := client.ServiceClient()
857+
opts := nodes.VirtualInterfaceOpts{
858+
ID: "1974dcfa-836f-41b2-b541-686c100900e5",
859+
}
860+
err := nodes.AttachVirtualInterface(context.TODO(), c, "1234asdf", opts).ExtractErr()
861+
th.AssertNoErr(t, err)
862+
}
863+
864+
func TestAttachVirtualInterfaceWithPort(t *testing.T) {
865+
th.SetupHTTP()
866+
defer th.TeardownHTTP()
867+
HandleAttachVirtualInterfaceWithPortSuccessfully(t)
868+
869+
c := client.ServiceClient()
870+
opts := nodes.VirtualInterfaceOpts{
871+
ID: "1974dcfa-836f-41b2-b541-686c100900e5",
872+
PortUUID: "b2f96298-5172-45e9-b174-8d1ba936ab47",
873+
}
874+
err := nodes.AttachVirtualInterface(context.TODO(), c, "1234asdf", opts).ExtractErr()
875+
th.AssertNoErr(t, err)
876+
}
877+
878+
func TestAttachVirtualInterfaceWithPortgroup(t *testing.T) {
879+
th.SetupHTTP()
880+
defer th.TeardownHTTP()
881+
HandleAttachVirtualInterfaceWithPortgroupSuccessfully(t)
882+
883+
c := client.ServiceClient()
884+
opts := nodes.VirtualInterfaceOpts{
885+
ID: "1974dcfa-836f-41b2-b541-686c100900e5",
886+
PortgroupUUID: "c24944b5-a52e-4c5c-9c0a-52a0235a08a2",
887+
}
888+
err := nodes.AttachVirtualInterface(context.TODO(), c, "1234asdf", opts).ExtractErr()
889+
th.AssertNoErr(t, err)
890+
}
891+
892+
func TestDetachVirtualInterface(t *testing.T) {
893+
th.SetupHTTP()
894+
defer th.TeardownHTTP()
895+
HandleDetachVirtualInterfaceSuccessfully(t)
896+
897+
c := client.ServiceClient()
898+
err := nodes.DetachVirtualInterface(context.TODO(), c, "1234asdf", "1974dcfa-836f-41b2-b541-686c100900e5").ExtractErr()
899+
th.AssertNoErr(t, err)
900+
}
901+
902+
func TestVirtualInterfaceOptsValidation(t *testing.T) {
903+
opts := nodes.VirtualInterfaceOpts{
904+
ID: "1974dcfa-836f-41b2-b541-686c100900e5",
905+
PortUUID: "b2f96298-5172-45e9-b174-8d1ba936ab47",
906+
PortgroupUUID: "c24944b5-a52e-4c5c-9c0a-52a0235a08a2",
907+
}
908+
909+
_, err := opts.ToVirtualInterfaceMap()
910+
th.AssertEquals(t, err.Error(), "cannot specify both port_uuid and portgroup_uuid")
911+
}

openstack/baremetal/v1/nodes/urls.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,11 @@ func firmwareListURL(client *gophercloud.ServiceClient, id string) string {
8989
func virtualMediaURL(client *gophercloud.ServiceClient, id string) string {
9090
return client.ServiceURL("nodes", id, "vmedia")
9191
}
92+
93+
func virtualInterfaceURL(client *gophercloud.ServiceClient, id string) string {
94+
return client.ServiceURL("nodes", id, "vifs")
95+
}
96+
97+
func virtualInterfaceDeleteURL(client *gophercloud.ServiceClient, id string, vifID string) string {
98+
return client.ServiceURL("nodes", id, "vifs", vifID)
99+
}

0 commit comments

Comments
 (0)