Skip to content

Commit a8a623f

Browse files
tatatoddmogren
authored andcommitted
Attempt to fix ipamd detached ENI issues (#1)
* seems about right * force removal tests
1 parent d31e2b5 commit a8a623f

3 files changed

Lines changed: 119 additions & 20 deletions

File tree

ipamd/datastore/data_store.go

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,18 @@ var (
7878
Help: "The number of IP addresses assigned to pods",
7979
},
8080
)
81+
forceRemovedENIs = prometheus.NewCounter(
82+
prometheus.CounterOpts{
83+
Name: "awscni_force_removed_enis",
84+
Help: "The number of ENIs force removed while they had assigned pods",
85+
},
86+
)
87+
forceRemovedIPs = prometheus.NewCounter(
88+
prometheus.CounterOpts{
89+
Name: "awscni_force_removed_ips",
90+
Help: "The number of IPs force removed while they had assigned pods",
91+
},
92+
)
8193
prometheusRegistered = false
8294
)
8395

@@ -146,6 +158,8 @@ func prometheusRegister() {
146158
prometheus.MustRegister(enis)
147159
prometheus.MustRegister(totalIPs)
148160
prometheus.MustRegister(assignedIPs)
161+
prometheus.MustRegister(forceRemovedENIs)
162+
prometheus.MustRegister(forceRemovedIPs)
149163
prometheusRegistered = true
150164
}
151165
}
@@ -209,7 +223,7 @@ func (ds *DataStore) AddIPv4AddressToStore(eniID string, ipv4 string) error {
209223
}
210224

211225
// DelIPv4AddressFromStore delete an IP of ENI from datastore
212-
func (ds *DataStore) DelIPv4AddressFromStore(eniID string, ipv4 string) error {
226+
func (ds *DataStore) DelIPv4AddressFromStore(eniID string, ipv4 string, force bool) error {
213227
ds.lock.Lock()
214228
defer ds.lock.Unlock()
215229
log.Debugf("Deleting ENI(%s)'s IPv4 address %s from datastore", eniID, ipv4)
@@ -226,7 +240,18 @@ func (ds *DataStore) DelIPv4AddressFromStore(eniID string, ipv4 string) error {
226240
}
227241

228242
if ipAddr.Assigned {
229-
return errors.New(IPInUseError)
243+
if !force {
244+
return errors.New(IPInUseError)
245+
}
246+
log.Warnf("Force deleting assigned ip %s on eni %s", ipv4, eniID)
247+
forceRemovedIPs.Inc()
248+
decrementAssignedCount(ds, curENI, ipAddr)
249+
for key, info := range ds.podsIP {
250+
if info.IP == ipv4 {
251+
delete(ds.podsIP, key)
252+
break
253+
}
254+
}
230255
}
231256

232257
ds.total--
@@ -307,6 +332,17 @@ func incrementAssignedCount(ds *DataStore, eni *ENIIPPool, addr *AddressInfo) {
307332
assignedIPs.Set(float64(ds.assigned))
308333
}
309334

335+
func decrementAssignedCount(ds *DataStore, eni *ENIIPPool, addr *AddressInfo) {
336+
ds.assigned--
337+
eni.AssignedIPv4Addresses--
338+
addr.Assigned = false
339+
curTime := time.Now()
340+
eni.lastUnassignedTime = curTime
341+
addr.UnassignedTime = curTime
342+
// Prometheus gauge
343+
assignedIPs.Set(float64(ds.assigned))
344+
}
345+
310346
// GetStats returns total number of IP addresses and number of assigned IP addresses
311347
func (ds *DataStore) GetStats() (int, int) {
312348
return ds.total, ds.assigned
@@ -431,7 +467,7 @@ func (ds *DataStore) RemoveUnusedENIFromStore(warmIPTarget int, minimumIPTarget
431467
}
432468

433469
// RemoveENIFromDataStore removes an ENI from the datastore. It return nil on success or an error.
434-
func (ds *DataStore) RemoveENIFromDataStore(eni string) error {
470+
func (ds *DataStore) RemoveENIFromDataStore(eni string, force bool) error {
435471
ds.lock.Lock()
436472
defer ds.lock.Unlock()
437473

@@ -440,9 +476,26 @@ func (ds *DataStore) RemoveENIFromDataStore(eni string) error {
440476
return errors.New(UnknownENIError)
441477
}
442478

443-
// Only unused ENIs can be deleted
444-
if eniIPPool.AssignedIPv4Addresses != 0 {
445-
return errors.New(ENIInUseError)
479+
if eniIPPool.hasPods() {
480+
if !force {
481+
return errors.New(ENIInUseError)
482+
}
483+
// This scenario can occur if the reconciliation process discovered this eni was detached
484+
// from the EC2 instance outside of the control of ipamd. If this happens, there's nothing
485+
// we can do other than force all pods to be unassigned from the IPs on this eni.
486+
log.Warnf("Force removing eni %s with %d assigned pods", eni, eniIPPool.AssignedIPv4Addresses)
487+
forceRemovedENIs.Inc()
488+
forceRemovedIPs.Add(float64(eniIPPool.AssignedIPv4Addresses))
489+
for _, addr := range eniIPPool.IPv4Addresses {
490+
if addr.Assigned {
491+
decrementAssignedCount(ds, eniIPPool, addr)
492+
}
493+
}
494+
for key, info := range ds.podsIP {
495+
if info.DeviceNumber == eniIPPool.DeviceNumber {
496+
delete(ds.podsIP, key)
497+
}
498+
}
446499
}
447500

448501
ds.total -= len(eniIPPool.IPv4Addresses)
@@ -478,13 +531,7 @@ func (ds *DataStore) UnassignPodIPv4Address(k8sPod *k8sapi.K8SPodInfo) (ip strin
478531
for _, eni := range ds.eniIPPools {
479532
ip, ok := eni.IPv4Addresses[ipAddr.IP]
480533
if ok && ip.Assigned {
481-
ip.Assigned = false
482-
ds.assigned--
483-
assignedIPs.Set(float64(ds.assigned))
484-
eni.AssignedIPv4Addresses--
485-
curTime := time.Now()
486-
ip.UnassignedTime = curTime
487-
eni.lastUnassignedTime = curTime
534+
decrementAssignedCount(ds, eni, ip)
488535
log.Infof("UnassignPodIPv4Address: pod (Name: %s, NameSpace %s Sandbox %s)'s ipAddr %s, DeviceNumber%d",
489536
k8sPod.Name, k8sPod.Namespace, k8sPod.Sandbox, ip.Address, eni.DeviceNumber)
490537
delete(ds.podsIP, podKey)

ipamd/datastore/data_store_test.go

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,37 @@ func TestDeleteENI(t *testing.T) {
5454
eniInfos := ds.GetENIInfos()
5555
assert.Equal(t, len(eniInfos.ENIIPPools), 3)
5656

57-
err = ds.RemoveENIFromDataStore("eni-2")
57+
err = ds.RemoveENIFromDataStore("eni-2", false)
5858
assert.NoError(t, err)
5959

6060
eniInfos = ds.GetENIInfos()
6161
assert.Equal(t, len(eniInfos.ENIIPPools), 2)
6262

63-
err = ds.RemoveENIFromDataStore("unknown-eni")
63+
err = ds.RemoveENIFromDataStore("unknown-eni", false)
6464
assert.Error(t, err)
6565

6666
eniInfos = ds.GetENIInfos()
6767
assert.Equal(t, len(eniInfos.ENIIPPools), 2)
68+
69+
// Add an IP and assign a pod.
70+
err = ds.AddIPv4AddressFromStore("eni-1", "1.1.1.1")
71+
assert.NoError(t, err)
72+
podInfo := &k8sapi.K8SPodInfo{
73+
Name: "pod-1",
74+
Namespace: "ns-1",
75+
IP: "1.1.1.1",
76+
}
77+
ip, device, err := ds.AssignPodIPv4Address(podInfo)
78+
assert.NoError(t, err)
79+
assert.Equal(t, "1.1.1.1", ip)
80+
assert.Equal(t, 1, device)
81+
82+
// Test force removal. The first call fails because eni-1 has an IP with a pod assigned to it,
83+
// but the second call force-removes it and succeeds.
84+
err = ds.RemoveENIFromDataStore("eni-1", false)
85+
assert.Error(t, err)
86+
err = ds.RemoveENIFromDataStore("eni-1", true)
87+
assert.NoError(t, err)
6888
}
6989

7090
func TestAddENIIPv4Address(t *testing.T) {
@@ -158,16 +178,39 @@ func TestDelENIIPv4Address(t *testing.T) {
158178
assert.Equal(t, ds.total, 3)
159179
assert.Equal(t, len(ds.eniIPPools["eni-1"].IPv4Addresses), 3)
160180

161-
err = ds.DelIPv4AddressFromStore("eni-1", "1.1.1.2")
181+
err = ds.DelIPv4AddressFromStore("eni-1", "1.1.1.2", false)
162182
assert.NoError(t, err)
163183
assert.Equal(t, ds.total, 2)
164184
assert.Equal(t, len(ds.eniIPPools["eni-1"].IPv4Addresses), 2)
165185

166186
// delete a unknown IP
167-
err = ds.DelIPv4AddressFromStore("eni-1", "10.10.10.10")
187+
err = ds.DelIPv4AddressFromStore("eni-1", "10.10.10.10", false)
188+
assert.Error(t, err)
189+
assert.Equal(t, ds.total, 2)
190+
assert.Equal(t, len(ds.eniIPPools["eni-1"].IPv4Addresses), 2)
191+
192+
// Assign a pod.
193+
podInfo := &k8sapi.K8SPodInfo{
194+
Name: "pod-1",
195+
Namespace: "ns-1",
196+
IP: "1.1.1.1",
197+
}
198+
ip, device, err := ds.AssignPodIPv4Address(podInfo)
199+
assert.NoError(t, err)
200+
assert.Equal(t, "1.1.1.1", ip)
201+
assert.Equal(t, 1, device)
202+
203+
// Test force removal. The first call fails because the IP has a pod assigned to it, but the
204+
// second call force-removes it and succeeds.
205+
err = ds.DelIPv4AddressFromStore("eni-1", "1.1.1.1", false)
168206
assert.Error(t, err)
169207
assert.Equal(t, ds.total, 2)
170208
assert.Equal(t, len(ds.eniIPPools["eni-1"].IPv4Addresses), 2)
209+
210+
err = ds.DelIPv4AddressFromStore("eni-1", "1.1.1.1", true)
211+
assert.NoError(t, err)
212+
assert.Equal(t, ds.total, 1)
213+
assert.Equal(t, len(ds.eniIPPools["eni-1"].IPv4Addresses), 1)
171214
}
172215

173216
func TestPodIPv4Address(t *testing.T) {

ipamd/ipamd.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,10 @@ func (c *IPAMContext) tryUnassignIPsFromAll() {
550550
// Delete IPs from datastore
551551
var deletedIPs []string
552552
for _, toDelete := range ips {
553-
err := c.dataStore.DelIPv4AddressFromStore(eniID, toDelete)
553+
// Don't force the delete, since a freeable IP might have been assigned to a pod
554+
// before we get around to deleting it.
555+
const force = false
556+
err := c.dataStore.DelIPv4AddressFromStore(eniID, toDelete, force)
554557
if err != nil {
555558
log.Warnf("Failed to delete IP %s on ENI %s from datastore: %s", toDelete, eniID, err)
556559
ipamdErrInc("decreaseIPPool")
@@ -991,7 +994,10 @@ func (c *IPAMContext) nodeIPPoolReconcile(interval time.Duration) {
991994
// Sweep phase: since the marked ENI have been removed, the remaining ones needs to be sweeped
992995
for eni := range curENIs.ENIIPPools {
993996
log.Infof("Reconcile and delete detached ENI %s", eni)
994-
err = c.dataStore.RemoveENIFromDataStore(eni)
997+
// Force the delete, since aws local metadata has told us that this ENI is no longer
998+
// attached, so any IPs assigned from this ENI will no longer work.
999+
const force = true
1000+
err = c.dataStore.RemoveENIFromDataStore(eni, force)
9951001
if err != nil {
9961002
log.Errorf("IP pool reconcile: Failed to delete ENI during reconcile: %v", err)
9971003
ipamdErrInc("eniReconcileDel")
@@ -1063,7 +1069,10 @@ func (c *IPAMContext) eniIPPoolReconcile(ipPool map[string]*datastore.AddressInf
10631069
// Sweep phase, delete remaining IPs
10641070
for existingIP := range ipPool {
10651071
log.Debugf("Reconcile and delete IP %s on ENI %s", existingIP, eni)
1066-
err := c.dataStore.DelIPv4AddressFromStore(eni, existingIP)
1072+
// Force the delete, since aws local metadata has told us that this ENI is no longer
1073+
// attached, so any IPs assigned from this ENI will no longer work.
1074+
const force = true
1075+
err := c.dataStore.DelIPv4AddressFromStore(eni, existingIP, force)
10671076
if err != nil {
10681077
log.Errorf("Failed to reconcile and delete IP %s on ENI %s, %v", existingIP, eni, err)
10691078
ipamdErrInc("ipReconcileDel")

0 commit comments

Comments
 (0)