diff --git a/go.mod b/go.mod index c16bf588d..71c0d6c33 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.36.3 github.com/pkg/errors v0.9.1 - github.com/spectrocloud/maas-client-go v0.1.4-beta1 + github.com/spectrocloud/maas-client-go v0.1.4-beta2 github.com/spf13/pflag v1.0.6 k8s.io/api v0.32.3 k8s.io/apiextensions-apiserver v0.32.3 diff --git a/go.sum b/go.sum index fe4a98dcf..c00e7d0a1 100644 --- a/go.sum +++ b/go.sum @@ -167,8 +167,8 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/spectrocloud/maas-client-go v0.1.4-beta1 h1:adYeve0oaYjiPuNnogMzw8LN9VenQK/iwNOO74H4lKE= -github.com/spectrocloud/maas-client-go v0.1.4-beta1/go.mod h1:LSxLlmaNCmkaldtysbp7Beq/O2wptBb6qE5iKj+Y7Lw= +github.com/spectrocloud/maas-client-go v0.1.4-beta2 h1:xiJ7UydMvFwI7CjsDbU50+oTZia0v8LuibiK/CMbyjo= +github.com/spectrocloud/maas-client-go v0.1.4-beta2/go.mod h1:LSxLlmaNCmkaldtysbp7Beq/O2wptBb6qE5iKj+Y7Lw= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= diff --git a/pkg/maas/client/mock/clienset_mock.go b/pkg/maas/client/mock/clienset_mock.go index 82ca8bdbd..ffcd50cad 100644 --- a/pkg/maas/client/mock/clienset_mock.go +++ b/pkg/maas/client/mock/clienset_mock.go @@ -93,6 +93,34 @@ func (mr *MockClientSetInterfaceMockRecorder) Domains() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Domains", reflect.TypeOf((*MockClientSetInterface)(nil).Domains)) } +// NetworkInterfaces mocks base method. +func (m *MockClientSetInterface) NetworkInterfaces() maasclient.NetworkInterfaces { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NetworkInterfaces") + ret0, _ := ret[0].(maasclient.NetworkInterfaces) + return ret0 +} + +// NetworkInterfaces indicates an expected call of NetworkInterfaces. +func (mr *MockClientSetInterfaceMockRecorder) NetworkInterfaces() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NetworkInterfaces", reflect.TypeOf((*MockClientSetInterface)(nil).NetworkInterfaces)) +} + +// IPAddresses mocks base method. +func (m *MockClientSetInterface) IPAddresses() maasclient.IPAddresses { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IPAddresses") + ret0, _ := ret[0].(maasclient.IPAddresses) + return ret0 +} + +// IPAddresses indicates an expected call of IPAddresses. +func (mr *MockClientSetInterfaceMockRecorder) IPAddresses() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IPAddresses", reflect.TypeOf((*MockClientSetInterface)(nil).IPAddresses)) +} + // Machines mocks base method. func (m *MockClientSetInterface) Machines() maasclient.Machines { m.ctrl.T.Helper() @@ -163,6 +191,48 @@ func (mr *MockClientSetInterfaceMockRecorder) Spaces() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Spaces", reflect.TypeOf((*MockClientSetInterface)(nil).Spaces)) } +// Tags mocks base method. +func (m *MockClientSetInterface) Tags() maasclient.Tags { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Tags") + ret0, _ := ret[0].(maasclient.Tags) + return ret0 +} + +// Tags indicates an expected call of Tags. +func (mr *MockClientSetInterfaceMockRecorder) Tags() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Tags", reflect.TypeOf((*MockClientSetInterface)(nil).Tags)) +} + +// VMHosts mocks base method. +func (m *MockClientSetInterface) VMHosts() maasclient.VMHosts { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "VMHosts") + ret0, _ := ret[0].(maasclient.VMHosts) + return ret0 +} + +// VMHosts indicates an expected call of VMHosts. +func (mr *MockClientSetInterfaceMockRecorder) VMHosts() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VMHosts", reflect.TypeOf((*MockClientSetInterface)(nil).VMHosts)) +} + +// Subnets mocks base method. +func (m *MockClientSetInterface) Subnets() maasclient.Subnets { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Subnets") + ret0, _ := ret[0].(maasclient.Subnets) + return ret0 +} + +// Subnets indicates an expected call of Subnets. +func (mr *MockClientSetInterfaceMockRecorder) Subnets() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subnets", reflect.TypeOf((*MockClientSetInterface)(nil).Subnets)) +} + // Users mocks base method. func (m *MockClientSetInterface) Users() maasclient.Users { m.ctrl.T.Helper() @@ -946,6 +1016,20 @@ func (mr *MockIPAddressMockRecorder) IP() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IP", reflect.TypeOf((*MockIPAddress)(nil).IP)) } +// InterfaceSet mocks base method. +func (m *MockIPAddress) InterfaceSet() []maasclient.NetworkInterface { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InterfaceSet") + ret0, _ := ret[0].([]maasclient.NetworkInterface) + return ret0 +} + +// InterfaceSet indicates an expected call of InterfaceSet. +func (mr *MockIPAddressMockRecorder) InterfaceSet() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InterfaceSet", reflect.TypeOf((*MockIPAddress)(nil).InterfaceSet)) +} + // MockZone is a mock of Zone interface. type MockZone struct { ctrl *gomock.Controller diff --git a/pkg/maas/dns/dns_test.go b/pkg/maas/dns/dns_test.go index c398d0153..dc0216bb1 100644 --- a/pkg/maas/dns/dns_test.go +++ b/pkg/maas/dns/dns_test.go @@ -33,6 +33,7 @@ func (f *fakeClientSet) Spaces() maasclient.Spaces { retur func (f *fakeClientSet) Users() maasclient.Users { return nil } func (f *fakeClientSet) Zones() maasclient.Zones { return nil } func (f *fakeClientSet) SSHKeys() maasclient.SSHKeys { return nil } +func (f *fakeClientSet) Subnets() maasclient.Subnets { return nil } func (f *fakeClientSet) VMHosts() maasclient.VMHosts { return nil } // fakeIPAddress satisfies maasclient.IPAddress for tests diff --git a/pkg/maas/machine/machine.go b/pkg/maas/machine/machine.go index fdb861544..c6eb59c9c 100644 --- a/pkg/maas/machine/machine.go +++ b/pkg/maas/machine/machine.go @@ -26,10 +26,10 @@ import ( // Service manages the MaaS machine var ( - ErrBrokenMachine = errors.New("broken machine encountered") - ErrVMComposing = errors.New("vm composing/commissioning") - reHostID = regexp.MustCompile(`host (\d+)`) - reMachineID = regexp.MustCompile(`machine[s]? ([a-z0-9]{4,6})`) + ErrBrokenMachine = errors.New("broken machine encountered") + ErrVMComposing = errors.New("vm composing/commissioning") + reHostID = regexp.MustCompile(`host (\d+)`) + reMachineID = regexp.MustCompile(`machine[s]? ([a-z0-9]{4,6})`) ) const ( @@ -497,6 +497,26 @@ func (s *Service) PrepareLXDVM(ctx context.Context) (*infrav1beta1.Machine, erro } } + // Before composing: check if the static IP is already allocated in MAAS + if s.scope.IsControlPlane() { + if staticIPToCheck := s.scope.GetStaticIP(); staticIPToCheck != "" { + s.scope.Info("Checking static IP availability before VM compose", "ip", staticIPToCheck) + existingIP, ipErr := s.maasClient.IPAddresses().GetAll(ctx, staticIPToCheck) + if ipErr == nil { + if ifaces := existingIP.InterfaceSet(); len(ifaces) > 0 { + return nil, fmt.Errorf("static IP %s is already in use (allocated to %d interface(s)); cannot compose VM — will retry when IP is available", staticIPToCheck, len(ifaces)) + } + // IP exists with no interfaces — stale/floating allocation; release before compose + s.scope.Info("Static IP has stale allocation with no interfaces; releasing before compose", "ip", staticIPToCheck) + if releaseErr := s.maasClient.IPAddresses().Release(ctx, staticIPToCheck); releaseErr != nil { + return nil, fmt.Errorf("static IP %s has a stale allocation that could not be released before compose: %w", staticIPToCheck, releaseErr) + } + s.scope.Info("Released stale IP allocation, proceeding with compose", "ip", staticIPToCheck) + } + // ipErr != nil means IP not found in MAAS — available, proceed normally + } + } + // Create the VM on the selected host m, err := selectedHost.Composer().Compose(ctx, params) if err != nil { @@ -827,7 +847,6 @@ func (s *Service) setMachineStaticIP(systemID string, config *infrav1beta1.Stati // Special handling for Commissioning state: skip static IP configuration to avoid blocking commissioning if machineState == "Commissioning" { s.scope.Info("Machine is commissioning, skipping static IP configuration to avoid interfering with commissioning process. Will configure after commissioning completes", "systemID", systemID) - // Return error to requeue - static IP will be configured after commissioning completes return fmt.Errorf("machine is commissioning, static IP configuration will be retried after commissioning completes") }