diff --git a/disk_helper_imagetransfer.go b/disk_helper_imagetransfer.go index 028c9ed..f4983b4 100644 --- a/disk_helper_imagetransfer.go +++ b/disk_helper_imagetransfer.go @@ -28,16 +28,16 @@ func generateCorrelationID(prefix string) string { // newImageTransfer creates a new image transfer for both uploads and downloads of images. It must be passed the // following parameters: // -// - cli is the oVirt SDK client. -// - logger is a logger from the go-ovirt-client-logger library -// - diskID is the ID of the disk that is being transferred to/from. -// - correlationID is an optional unique ID that can be used to check if the job completed. If no correlation ID is -// passed, this function generates a random one. -// - retries is a list of retry strategies to use for each API call. -// - direction is the direction of transfer. See ovirtsdk.ImageTransferDirection. -// - format is the disk format being uploaded, or the disk format requested. The oVirt Engine will automatically convert -// images to the requested format. -// - updateDisk is a function that will be called whenever the disk object is updated. +// - cli is the oVirt SDK client. +// - logger is a logger from the go-ovirt-client-logger library +// - diskID is the ID of the disk that is being transferred to/from. +// - correlationID is an optional unique ID that can be used to check if the job completed. If no correlation ID is +// passed, this function generates a random one. +// - retries is a list of retry strategies to use for each API call. +// - direction is the direction of transfer. See ovirtsdk.ImageTransferDirection. +// - format is the disk format being uploaded, or the disk format requested. The oVirt Engine will automatically convert +// images to the requested format. +// - updateDisk is a function that will be called whenever the disk object is updated. func newImageTransfer( cli *oVirtClient, logger Logger, diff --git a/new.go b/new.go index b197ae8..dfc4d39 100644 --- a/new.go +++ b/new.go @@ -77,63 +77,63 @@ func (e *extraSettings) WithProxy(addr string) ExtraSettingsBuilder { // New creates a new copy of the enhanced oVirt client. It accepts the following options: // -// url +// url // // This is the oVirt engine URL. This must start with http:// or https:// and typically ends with /ovirt-engine/. // -// username +// username // // This is the username for the oVirt engine. This must contain the profile separated with an @ sign. For example, // admin@internal. // -// password +// password // // This is the password for the oVirt engine. Other authentication mechanisms are not supported. // -// tls +// tls // // This is a TLSProvider responsible for supplying TLS configuration to the client. See below for a simple example. // -// logger +// logger // // This is an implementation of ovirtclientlog.Logger to provide logging. // -// extraSettings +// extraSettings // // This is an implementation of the ExtraSettings interface, allowing for customization of headers and turning on // compression. // -// TLS +// # TLS // // This library tries to follow best practices when it comes to connection security. Therefore, you will need to pass // a valid implementation of the TLSProvider interface in the tls parameter. The easiest way to do this is calling // the ovirtclient.TLS() function and then configuring the resulting variable with the following functions: // -// tls := ovirtclient.TLS() +// tls := ovirtclient.TLS() // -// // Add certificates from an in-memory byte slice. Certificates must be in PEM format. -// tls.CACertsFromMemory(caCerts) +// // Add certificates from an in-memory byte slice. Certificates must be in PEM format. +// tls.CACertsFromMemory(caCerts) // -// // Add certificates from a single file. Certificates must be in PEM format. -// tls.CACertsFromFile("/path/to/file.pem") +// // Add certificates from a single file. Certificates must be in PEM format. +// tls.CACertsFromFile("/path/to/file.pem") // -// // Add certificates from a directory. Optionally, regular expressions can be passed that must match the file -// // names. -// tls.CACertsFromDir("/path/to/certs", regexp.MustCompile(`\.pem`)) +// // Add certificates from a directory. Optionally, regular expressions can be passed that must match the file +// // names. +// tls.CACertsFromDir("/path/to/certs", regexp.MustCompile(`\.pem`)) // -// // Add system certificates -// tls.CACertsFromSystem() +// // Add system certificates +// tls.CACertsFromSystem() // -// // Disable certificate verification. This is a bad idea. -// tls.Insecure() +// // Disable certificate verification. This is a bad idea. +// tls.Insecure() // -// client, err := ovirtclient.New( -// url, username, password, -// tls, -// logger, extraSettings -// ) +// client, err := ovirtclient.New( +// url, username, password, +// tls, +// logger, extraSettings +// ) // -// Extra settings +// # Extra settings // // This library also supports customizing the connection settings. In order to stay backwards compatible the // extraSettings parameter must implement the ovirtclient.ExtraSettings interface. Future versions of this library will diff --git a/newmock.go b/newmock.go index 1c03839..9371efe 100644 --- a/newmock.go +++ b/newmock.go @@ -11,6 +11,7 @@ import ( // NewMock creates a new in-memory mock client. This client can be used as a testing facility for // higher level code. +// //goland:noinspection GoUnusedExportedFunction func NewMock() MockClient { return NewMockWithLogger(&noopLogger{}) diff --git a/nic.go b/nic.go index dc5abed..8b9a8fd 100644 --- a/nic.go +++ b/nic.go @@ -33,12 +33,21 @@ type NICClient interface { } // OptionalNICParameters is an interface that declares the source of optional parameters for NIC creation. -type OptionalNICParameters interface{} +type OptionalNICParameters interface { + // represent mac_address for NIC + Mac() string +} // BuildableNICParameters is a modifiable version of OptionalNICParameters. You can use CreateNICParams() to create a // new copy, or implement your own. type BuildableNICParameters interface { OptionalNICParameters + + // WithMac sets macAddress for the NIC. + WithMac(mac string) (BuildableNICParameters, error) + + // MustWithMac is the same as WithMac, but panics instead of returning an error. + MustWithMac(mac string) BuildableNICParameters } // CreateNICParams returns a buildable structure of OptionalNICParameters. @@ -46,7 +55,26 @@ func CreateNICParams() BuildableNICParameters { return &nicParams{} } -type nicParams struct{} +type nicParams struct { + mac string +} + +func (c *nicParams) Mac() string { + return c.mac +} + +func (c *nicParams) WithMac(mac string) (BuildableNICParameters, error) { + c.mac = mac + return c, nil +} + +func (c *nicParams) MustWithMac(mac string) BuildableNICParameters { + builder, err := c.WithMac(mac) + if err != nil { + panic(err) + } + return builder +} // UpdateNICParameters is an interface that declares methods of changeable parameters for NIC's. Each // method can return nil to leave an attribute unchanged, or a new value for the attribute. @@ -56,6 +84,9 @@ type UpdateNICParameters interface { // VNICProfileID potentially returns a change VNIC profile for a NIC. VNICProfileID() *VNICProfileID + + // Mac potentially returns a change MacAddress for a nic + Mac() *string } // BuildableUpdateNICParameters is a buildable version of UpdateNICParameters. @@ -71,6 +102,11 @@ type BuildableUpdateNICParameters interface { WithVNICProfileID(id VNICProfileID) (BuildableUpdateNICParameters, error) // MustWithVNICProfileID is identical to WithVNICProfileID, but panics instead of returning an error. MustWithVNICProfileID(id VNICProfileID) BuildableUpdateNICParameters + + // WithMac sets MaAddress of a NIC for the UpdateNIC method. + WithMac(mac string) (BuildableUpdateNICParameters, error) + // MustWithMac is identical to WithMac, but panics instead of returning an error. + MustWithMac(mac string) BuildableUpdateNICParameters } // UpdateNICParams creates a buildable UpdateNICParameters. @@ -81,6 +117,7 @@ func UpdateNICParams() BuildableUpdateNICParameters { type updateNICParams struct { name *string vnicProfileID *VNICProfileID + mac *string } func (u *updateNICParams) Name() *string { @@ -91,6 +128,10 @@ func (u *updateNICParams) VNICProfileID() *VNICProfileID { return u.vnicProfileID } +func (u *updateNICParams) Mac() *string { + return u.mac +} + func (u *updateNICParams) WithName(name string) (BuildableUpdateNICParameters, error) { u.name = &name return u, nil @@ -117,6 +158,19 @@ func (u *updateNICParams) MustWithVNICProfileID(id VNICProfileID) BuildableUpdat return b } +func (u *updateNICParams) WithMac(mac string) (BuildableUpdateNICParameters, error) { + u.mac = &mac + return u, nil +} + +func (u *updateNICParams) MustWithMac(mac string) BuildableUpdateNICParameters { + b, err := u.WithMac(mac) + if err != nil { + panic(err) + } + return b +} + // NICData is the core of NIC which only provides data-access functions. type NICData interface { // ID is the identifier for this network interface. @@ -127,6 +181,8 @@ type NICData interface { VMID() VMID // VNICProfileID returns the ID of the VNIC profile in use by the NIC. VNICProfileID() VNICProfileID + // Mac returns a MacAddress for a nic + Mac() string } // NIC represents a network interface. @@ -170,12 +226,21 @@ func convertSDKNIC(sdkObject *ovirtsdk.Nic, cli Client) (NIC, error) { if !ok { return nil, newFieldNotFound("vNIC Profile on VM", "ID") } + mac, ok := sdkObject.Mac() + if !ok { + return nil, newFieldNotFound("mac", "NIC") + } + macAddr, ok := mac.Address() + if !ok { + return nil, newFieldNotFound("address", "mac") + } return &nic{ cli, NICID(id), name, VMID(vmid), VNICProfileID(vnicProfileID), + macAddr, }, nil } @@ -186,6 +251,7 @@ type nic struct { name string vmid VMID vnicProfileID VNICProfileID + mac string } func (n nic) Update(params UpdateNICParameters, retries ...RetryStrategy) (NIC, error) { @@ -216,6 +282,10 @@ func (n nic) VMID() VMID { return n.vmid } +func (n nic) Mac() string { + return n.mac +} + func (n nic) Remove(retries ...RetryStrategy) error { return n.client.RemoveNIC(n.vmid, n.id, retries...) } @@ -227,6 +297,7 @@ func (n nic) withName(name string) *nic { name: name, vmid: n.vmid, vnicProfileID: n.vnicProfileID, + mac: n.mac, } } @@ -237,5 +308,17 @@ func (n nic) withVNICProfileID(vnicProfileID VNICProfileID) *nic { name: n.name, vmid: n.vmid, vnicProfileID: vnicProfileID, + mac: n.mac, + } +} + +func (n nic) withMac(mac string) *nic { + return &nic{ + client: n.client, + id: n.id, + name: n.name, + vmid: n.vmid, + vnicProfileID: n.vnicProfileID, + mac: mac, } } diff --git a/nic_create.go b/nic_create.go index 38329e3..b13571c 100644 --- a/nic_create.go +++ b/nic_create.go @@ -2,6 +2,7 @@ package ovirtclient import ( "fmt" + "net" "github.com/google/uuid" ovirtsdk "github.com/ovirt/go-ovirt" @@ -11,13 +12,21 @@ func (o *oVirtClient) CreateNIC( vmid VMID, vnicProfileID VNICProfileID, name string, - _ OptionalNICParameters, + params OptionalNICParameters, retries ...RetryStrategy, ) (result NIC, err error) { if err := validateNICCreationParameters(vmid, name); err != nil { return nil, err } + var mac string + if params != nil { + if err := validateNICCreationOptionalParameters(params); err != nil { + return nil, err + } + mac = params.Mac() + } + retries = defaultRetries(retries, defaultReadTimeouts(o)) err = retry( fmt.Sprintf("creating NIC for VM %s", vmid), @@ -27,6 +36,11 @@ func (o *oVirtClient) CreateNIC( nicBuilder := ovirtsdk.NewNicBuilder() nicBuilder.Name(name) nicBuilder.VnicProfile(ovirtsdk.NewVnicProfileBuilder().Id(string(vnicProfileID)).MustBuild()) + + if mac != "" { + nicBuilder.Mac(ovirtsdk.NewMacBuilder().Address(mac).MustBuild()) + } + nic := nicBuilder.MustBuild() response, err := o.conn.SystemService().VmsService().VmService(string(vmid)).NicsService().Add().Nic(nic).Send() @@ -60,7 +74,7 @@ func (m *mockClient) CreateNIC( vmid VMID, vnicProfileID VNICProfileID, name string, - _ OptionalNICParameters, + params OptionalNICParameters, _ ...RetryStrategy, ) (NIC, error) { m.lock.Lock() @@ -87,6 +101,14 @@ func (m *mockClient) CreateNIC( vmid: vmid, vnicProfileID: vnicProfileID, } + + if params != nil { + if err := validateNICCreationOptionalParameters(params); err != nil { + return nil, err + } + nic.mac = params.Mac() + } + m.nics[id] = nic return nic, nil @@ -101,3 +123,12 @@ func validateNICCreationParameters(vmid VMID, name string) error { } return nil } + +func validateNICCreationOptionalParameters(params OptionalNICParameters) error { + if mac := params.Mac(); mac != "" { + if _, err := net.ParseMAC(mac); err != nil { + return newError(EUnidentified, "Failed to parse MacAddress: %s", mac) + } + } + return nil +} diff --git a/nic_create_test.go b/nic_create_test.go index fe001d2..fc0fe5d 100644 --- a/nic_create_test.go +++ b/nic_create_test.go @@ -58,3 +58,32 @@ func TestDuplicateVMNICCreation(t *testing.T) { assertCanRemoveNIC(t, nic1) assertNICCount(t, vm, 0) } + +func TestVMNICWithMacCreation(t *testing.T) { + t.Parallel() + helper := getHelper(t) + mac := "a1:b2:c3:d4:e5:f6" + invalidMac := "invalid mac address" + + vm := assertCanCreateVM( + t, + helper, + fmt.Sprintf("nic_test_%s", helper.GenerateRandomID(5)), + ovirtclient.CreateVMParams(), + ) + assertNICCount(t, vm, 0) + nic := assertCanCreateNICMac( + t, + helper, + vm, + mac) + assertNICCount(t, vm, 1) + assertCantCreateNICMac( + t, + helper, + vm, + invalidMac) + assertNICCount(t, vm, 1) + assertCanRemoveNIC(t, nic) + assertNICCount(t, vm, 0) +} diff --git a/nic_test.go b/nic_test.go index 1297782..474ac4d 100644 --- a/nic_test.go +++ b/nic_test.go @@ -1,6 +1,7 @@ package ovirtclient_test import ( + "fmt" "testing" ovirtclient "github.com/ovirt/go-ovirt-client/v2" @@ -31,6 +32,11 @@ func assertCanCreateNIC( if nic.VMID() != vm.ID() { t.Fatalf("VM ID mismatch between NIC and VM (%s != %s)", nic.VMID(), vm.ID()) } + if params != nil { + if params.Mac() != "" && params.Mac() != nic.Mac() { + t.Fatalf("Failed to create NIC with custom mac address. Expected '%s', but created mac is '%s'", params.Mac(), nic.Mac()) + } + } return nic } @@ -41,10 +47,14 @@ func assertCannotCreateNIC( name string, params ovirtclient.BuildableNICParameters, ) { - nic, _ := vm.CreateNIC(name, helper.GetVNICProfileID(), params) + nic, err := vm.CreateNIC(name, helper.GetVNICProfileID(), params) if nic != nil { t.Fatalf("create 2 NICs with same name %s", name) } + if err != nil { + print(err) + } + } func assertCanRemoveNIC(t *testing.T, nic ovirtclient.NIC) { @@ -73,3 +83,67 @@ func assertCanUpdateNICVNICProfile(t *testing.T, nic ovirtclient.NIC, vnicProfil } return newNIC } + +func assertCanUpdateNICMac(t *testing.T, nic ovirtclient.NIC, mac string) ovirtclient.NIC { + newNIC, err := nic.Update(ovirtclient.UpdateNICParams().MustWithMac(mac)) + if err != nil { + t.Fatalf("failed to update NIC (%v)", err) + } + if newNIC.Mac() != mac { + t.Fatalf("NIC MacAddress not changed after update call") + } + return newNIC +} + +func assertCantUpdateNICMac(t *testing.T, nic ovirtclient.NIC, mac string) ovirtclient.NIC { + newNIC, err := nic.Update(ovirtclient.UpdateNICParams().MustWithMac(mac)) + if err == nil { + t.Fatalf("Mac address validation error. Invalid mac was accepted: %s", mac) + } + return newNIC +} + +func assertCanCreateNICMac( + t *testing.T, + helper ovirtclient.TestHelper, + vm ovirtclient.VM, + mac string, +) ovirtclient.NIC { + params := ovirtclient.CreateNICParams() + params, err := params.WithMac(mac) + + if err != nil { + t.Fatalf("Failed to set custom Mac address on NIC. Error: %s", err) + } + + nic, err := vm.CreateNIC(fmt.Sprintf("test-%s", helper.GenerateRandomID(5)), helper.GetVNICProfileID(), params) + if err != nil { + t.Fatalf("failed to create NIC on VM %s", vm.ID()) + } + if nic.VMID() != vm.ID() { + t.Fatalf("VM ID mismatch between NIC and VM (%s != %s)", nic.VMID(), vm.ID()) + } + if nic.Mac() != params.Mac() { + t.Fatalf("Failed to create NIC with custom mac address: %s", nic.Mac()) + } + + return nic +} + +func assertCantCreateNICMac( + t *testing.T, + helper ovirtclient.TestHelper, + vm ovirtclient.VM, + mac string, +) ovirtclient.NIC { + params := ovirtclient.CreateNICParams() + params, err := params.WithMac(mac) + if err != nil { + t.Fatalf("Failed to set custom Mac address on NIC. Error: %s", err) + } + nic, err := vm.CreateNIC(fmt.Sprintf("test-%s", helper.GenerateRandomID(5)), helper.GetVNICProfileID(), params) + if err == nil { + t.Fatalf("Mac address validation error. Invalid mac was accepted: %s", mac) + } + return nic +} diff --git a/nic_update.go b/nic_update.go index d1c4e5d..c9784cc 100644 --- a/nic_update.go +++ b/nic_update.go @@ -2,6 +2,7 @@ package ovirtclient import ( "fmt" + "net" ovirtsdk "github.com/ovirt/go-ovirt" ) @@ -21,6 +22,12 @@ func (o *oVirtClient) UpdateNIC( if vnicProfileID := params.VNICProfileID(); vnicProfileID != nil { nicBuilder.VnicProfile(ovirtsdk.NewVnicProfileBuilder().Id(string(*vnicProfileID)).MustBuild()) } + if mac := params.Mac(); mac != nil { + if _, err := net.ParseMAC(*mac); err != nil { + return nil, newError(EUnidentified, "Failed to parse MacAddress: %s", *mac) + } + nicBuilder.Mac(ovirtsdk.NewMacBuilder().Address(*mac).MustBuild()) + } req.Nic(nicBuilder.MustBuild()) @@ -70,6 +77,12 @@ func (m *mockClient) UpdateNIC(vmid VMID, nicID NICID, params UpdateNICParameter } nic = nic.withVNICProfileID(*vnicProfileID) } + if mac := params.Mac(); mac != nil { + if _, err := net.ParseMAC(*mac); err != nil { + return nil, newError(EUnidentified, "Failed to parse MacAddress: %s", *mac) + } + nic = nic.withMac(*mac) + } m.nics[nicID] = nic return nic, nil diff --git a/nic_update_test.go b/nic_update_test.go index eb5cbc9..0aa8390 100644 --- a/nic_update_test.go +++ b/nic_update_test.go @@ -28,6 +28,8 @@ func TestVMNICUpdate(t *testing.T) { nic = assertCanUpdateNICName(t, nic, fmt.Sprintf("test-%s", helper.GenerateRandomID(5))) vnicProfile := assertCanCreateVNICProfile(t, helper) nic = assertCanUpdateNICVNICProfile(t, nic, vnicProfile.ID()) + nic = assertCanUpdateNICMac(t, nic, "a1:b2:c3:d4:e5:f6") + _ = assertCantUpdateNICMac(t, nic, "invalid mac address") // Go back to the original VNIC profile ID to make sure we don't block deleting the test VNIC profile. _ = assertCanUpdateNICVNICProfile(t, nic, helper.GetVNICProfileID()) } diff --git a/util_test_helper.go b/util_test_helper.go index 918bbfc..d12e446 100644 --- a/util_test_helper.go +++ b/util_test_helper.go @@ -505,43 +505,43 @@ func NewMockTestHelper(logger ovirtclientlog.Logger) (TestHelper, error) { // NewLiveTestHelperFromEnv is a function that creates a test helper working against a live (not mock) // oVirt engine using environment variables. The environment variables are as follows: // -// OVIRT_URL +// OVIRT_URL // // URL of the oVirt engine. Mandatory. // -// OVIRT_USERNAME +// OVIRT_USERNAME // // The username for the oVirt engine. Mandatory. // -// OVIRT_PASSWORD +// OVIRT_PASSWORD // // The password for the oVirt engine. Mandatory. // -// OVIRT_CAFILE +// OVIRT_CAFILE // // A file containing the CA certificate in PEM format. // -// OVIRT_CA_BUNDLE +// OVIRT_CA_BUNDLE // // Provide the CA certificate in PEM format directly. // -// OVIRT_INSECURE +// OVIRT_INSECURE // // Disable certificate verification if set. Not recommended. // -// OVIRT_CLUSTER_ID +// OVIRT_CLUSTER_ID // // The cluster to use for testing. Will be automatically chosen if not provided. // -// OVIRT_BLANK_TEMPLATE_ID +// OVIRT_BLANK_TEMPLATE_ID // // ID of the blank template. Will be automatically chosen if not provided. // -// OVIRT_STORAGE_DOMAIN_ID +// OVIRT_STORAGE_DOMAIN_ID // // Storage domain to use for testing. Will be automatically chosen if not provided. // -// OVIRT_VNIC_PROFILE_ID +// OVIRT_VNIC_PROFILE_ID // // VNIC profile to use for testing. Will be automatically chosen if not provided. func NewLiveTestHelperFromEnv(logger ovirtclientlog.Logger) (TestHelper, error) { diff --git a/util_wait_for_job_finished.go b/util_wait_for_job_finished.go index 5baab43..bf0c6a7 100644 --- a/util_wait_for_job_finished.go +++ b/util_wait_for_job_finished.go @@ -13,14 +13,14 @@ import ( // under 30 chars. To set a correlationID add `Query("correlation_id", correlationID)` to the engine API call, for // example: // -// correlationID := fmt.Sprintf("image_transfer_%s", utilrand.String(5)) -// conn. -// SystemService(). -// DisksService(). -// DiskService(diskId). -// Update(). -// Query("correlation_id", correlationID). -// Send() +// correlationID := fmt.Sprintf("image_transfer_%s", utilrand.String(5)) +// conn. +// SystemService(). +// DisksService(). +// DiskService(diskId). +// Update(). +// Query("correlation_id", correlationID). +// Send() func (o *oVirtClient) waitForJobFinished(correlationID string, retries []RetryStrategy) error { return retry( fmt.Sprintf("waiting for job with correlation ID %s to finish", correlationID),