@@ -14,6 +14,8 @@ import (
1414 osextendedstatus "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/extendedstatus"
1515 "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs"
1616 osservers "github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
17+ osfloatingips "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips"
18+ osnetworks "github.com/gophercloud/gophercloud/openstack/networking/v2/networks"
1719 "github.com/gophercloud/gophercloud/pagination"
1820
1921 "github.com/kubermatic/machine-controller/pkg/cloudprovider/cloud"
@@ -24,12 +26,18 @@ import (
2426 "k8s.io/apimachinery/pkg/runtime"
2527 "k8s.io/apimachinery/pkg/types"
2628 utilruntime "k8s.io/apimachinery/pkg/util/runtime"
29+ "k8s.io/apimachinery/pkg/util/sets"
2730 "k8s.io/apimachinery/pkg/util/wait"
2831
2932 "sigs.k8s.io/cluster-api/pkg/apis/cluster/common"
3033 "sigs.k8s.io/cluster-api/pkg/apis/cluster/v1alpha1"
3134)
3235
36+ const (
37+ floatingIPReleaseFinalizer = "kubermatic.io/release-openstack-floating-ip"
38+ floatingIPIDAnnotationKey = "kubermatic.io/release-openstack-floating-ip"
39+ )
40+
3341type provider struct {
3442 configVarResolver * providerconfig.ConfigVarResolver
3543}
@@ -349,7 +357,7 @@ func (p *provider) Validate(spec v1alpha1.MachineSpec) error {
349357 return nil
350358}
351359
352- func (p * provider ) Create (machine * v1alpha1.Machine , _ * cloud.MachineCreateDeleteData , userdata string ) (instance.Instance , error ) {
360+ func (p * provider ) Create (machine * v1alpha1.Machine , machineCreateDeleteData * cloud.MachineCreateDeleteData , userdata string ) (instance.Instance , error ) {
353361 c , _ , _ , err := p .getConfig (machine .Spec .ProviderConfig )
354362 if err != nil {
355363 return nil , cloudprovidererrors.TerminalError {
@@ -423,7 +431,7 @@ func (p *provider) Create(machine *v1alpha1.Machine, _ *cloud.MachineCreateDelet
423431
424432 // Find a free FloatingIP or allocate a new one
425433 if c .FloatingIPPool != "" {
426- if err := assignFloatingIPToInstance (client , server .ID , c .FloatingIPPool , c .Region , network ); err != nil {
434+ if err := assignFloatingIPToInstance (machineCreateDeleteData . Updater , machine , client , server .ID , c .FloatingIPPool , c .Region , network ); err != nil {
427435 defer deleteInstanceDueToFatalLogged (computeClient , server .ID )
428436 return nil , fmt .Errorf ("failed to assign a floating ip to instance %s: %v" , server .ID , err )
429437 }
@@ -475,10 +483,20 @@ func deleteInstanceDueToFatalLogged(computeClient *gophercloud.ServiceClient, se
475483 glog .V (0 ).Infof ("Instance %s got deleted" , serverID )
476484}
477485
478- func (p * provider ) Cleanup (machine * v1alpha1.Machine , _ * cloud.MachineCreateDeleteData ) (bool , error ) {
486+ func (p * provider ) Cleanup (machine * v1alpha1.Machine , machineCreateDeleteData * cloud.MachineCreateDeleteData ) (bool , error ) {
487+ var hasFloatingIPReleaseFinalizer bool
488+ if finalizers := sets .NewString (machine .Finalizers ... ); finalizers .Has (floatingIPReleaseFinalizer ) {
489+ hasFloatingIPReleaseFinalizer = true
490+ }
491+
479492 instance , err := p .Get (machine )
480493 if err != nil {
481494 if err == cloudprovidererrors .ErrInstanceNotFound {
495+ if hasFloatingIPReleaseFinalizer {
496+ if err := p .cleanupFloatingIP (machine , machineCreateDeleteData .Updater ); err != nil {
497+ return false , fmt .Errorf ("failed to clean up floating ip: %v" , err )
498+ }
499+ }
482500 return true , nil
483501 }
484502 return false , err
@@ -502,10 +520,14 @@ func (p *provider) Cleanup(machine *v1alpha1.Machine, _ *cloud.MachineCreateDele
502520 return false , osErrorToTerminalError (err , "failed to get compute client" )
503521 }
504522
505- if err := osservers .Delete (computeClient , instance .ID ()).ExtractErr (); err != nil {
523+ if err := osservers .Delete (computeClient , instance .ID ()).ExtractErr (); err != nil && err . Error () != "Resource not found" {
506524 return false , osErrorToTerminalError (err , "failed to delete instance" )
507525 }
508526
527+ if hasFloatingIPReleaseFinalizer {
528+ return false , p .cleanupFloatingIP (machine , machineCreateDeleteData .Updater )
529+ }
530+
509531 return false , nil
510532}
511533
@@ -733,3 +755,109 @@ type forbiddenResponse struct {
733755 Code int `json:"code"`
734756 } `json:"forbidden"`
735757}
758+
759+ func (p * provider ) cleanupFloatingIP (machine * v1alpha1.Machine , updater cloud.MachineUpdater ) error {
760+ floatingIPID , exists := machine .Annotations [floatingIPIDAnnotationKey ]
761+ if ! exists {
762+ return osErrorToTerminalError (fmt .Errorf ("failed to release floating ip" ),
763+ fmt .Sprintf ("%s finalizer exists but %s annotation does not" , floatingIPReleaseFinalizer , floatingIPIDAnnotationKey ))
764+ }
765+
766+ c , _ , _ , err := p .getConfig (machine .Spec .ProviderConfig )
767+ if err != nil {
768+ return cloudprovidererrors.TerminalError {
769+ Reason : common .InvalidConfigurationMachineError ,
770+ Message : fmt .Sprintf ("Failed to parse MachineSpec, due to %v" , err ),
771+ }
772+ }
773+
774+ client , err := getClient (c )
775+ if err != nil {
776+ return osErrorToTerminalError (err , "failed to get a openstack client" )
777+ }
778+ netClient , err := goopenstack .NewNetworkV2 (client , gophercloud.EndpointOpts {Region : c .Region })
779+ if err != nil {
780+ return fmt .Errorf ("failed to create the networkv2 client for region %s: %v" , c .Region , err )
781+ }
782+ if err := osfloatingips .Delete (netClient , floatingIPID ).ExtractErr (); err != nil && err .Error () != "Resource not found" {
783+ return fmt .Errorf ("failed to delete floating ip %s: %v" , floatingIPID , err )
784+ }
785+ if _ , err := updater (machine , func (m * v1alpha1.Machine ) {
786+ finalizers := sets .NewString (m .Finalizers ... )
787+ finalizers .Delete (floatingIPReleaseFinalizer )
788+ m .Finalizers = finalizers .List ()
789+ }); err != nil {
790+ return fmt .Errorf ("failed to delete %s finalizer from Machine: %v" , floatingIPReleaseFinalizer , err )
791+ }
792+
793+ return nil
794+ }
795+
796+ func assignFloatingIPToInstance (machineUpdater cloud.MachineUpdater , machine * v1alpha1.Machine , client * gophercloud.ProviderClient , instanceID , floatingIPPoolName , region string , network * osnetworks.Network ) error {
797+ port , err := getInstancePort (client , region , instanceID , network .ID )
798+ if err != nil {
799+ return fmt .Errorf ("failed to get instance port for network %s in region %s: %v" , network .ID , region , err )
800+ }
801+
802+ netClient , err := goopenstack .NewNetworkV2 (client , gophercloud.EndpointOpts {Region : region })
803+ if err != nil {
804+ return fmt .Errorf ("failed to create the networkv2 client for region %s: %v" , region , err )
805+ }
806+
807+ floatingIPPool , err := getNetwork (client , region , floatingIPPoolName )
808+ if err != nil {
809+ return osErrorToTerminalError (err , fmt .Sprintf ("failed to get floating ip pool %q" , floatingIPPoolName ))
810+ }
811+
812+ // We're only interested in the part which is vulnerable to concurrent access
813+ started := time .Now ()
814+ glog .V (2 ).Infof ("Assigning a floating IP to instance %s" , instanceID )
815+
816+ floatingIPAssignLock .Lock ()
817+ defer floatingIPAssignLock .Unlock ()
818+
819+ freeFloatingIps , err := getFreeFloatingIPs (client , region , floatingIPPool )
820+ if err != nil {
821+ return osErrorToTerminalError (err , "failed to get free floating ips" )
822+ }
823+
824+ var ip * osfloatingips.FloatingIP
825+ if len (freeFloatingIps ) < 1 {
826+ if ip , err = createFloatingIP (client , region , port .ID , floatingIPPool ); err != nil {
827+ return osErrorToTerminalError (err , "failed to allocate a floating ip" )
828+ }
829+ if _ , err = machineUpdater (machine , func (m * v1alpha1.Machine ) {
830+ m .Finalizers = append (m .Finalizers , floatingIPReleaseFinalizer )
831+ if m .Annotations == nil {
832+ m .Annotations = map [string ]string {}
833+ }
834+ m .Annotations [floatingIPIDAnnotationKey ] = ip .ID
835+ }); err != nil {
836+ return fmt .Errorf ("failed to add floating ip release finalizer after allocating floating ip: %v" , err )
837+ }
838+ } else {
839+ freeIP := freeFloatingIps [0 ]
840+ ip , err = osfloatingips .Update (netClient , freeIP .ID , osfloatingips.UpdateOpts {
841+ PortID : & port .ID ,
842+ }).Extract ()
843+ if err != nil {
844+ return fmt .Errorf ("failed to update FloatingIP %s(%s): %v" , freeIP .ID , freeIP .FloatingIP , err )
845+ }
846+
847+ // We're now going to wait 3 seconds and check if the IP is still ours. If not, we're going to fail
848+ // On our reference system it took ~3 seconds for a full FloatingIP allocation (Including creating a new one). It took ~600ms just for assigning one.
849+ time .Sleep (floatingReassignIPCheckPeriod )
850+ currentIP , err := osfloatingips .Get (netClient , ip .ID ).Extract ()
851+ if err != nil {
852+ return fmt .Errorf ("failed to load FloatingIP %s after assignment has been done: %v" , ip .FloatingIP , err )
853+ }
854+ // Verify if the port is still the one we set it to
855+ if currentIP .PortID != port .ID {
856+ return fmt .Errorf ("floatingIP %s got reassigned" , currentIP .FloatingIP )
857+ }
858+ }
859+ secondsTook := time .Since (started ).Seconds ()
860+
861+ glog .V (2 ).Infof ("Successfully assigned the FloatingIP %s to instance %s. Took %f seconds(without the recheck wait period %f seconds). " , ip .FloatingIP , instanceID , secondsTook , floatingReassignIPCheckPeriod .Seconds ())
862+ return nil
863+ }
0 commit comments