Skip to content

Commit 496d4a1

Browse files
o/ifacestate: properly handle undo scenarios where auto-connections are dropped (#17018)
When a refresh moves to a revision that drops an auto-connected plug, reloadConnections removes that auto-connection from the connection state. If the refresh fails, the deleted auto-connection is not restored. This leaves the interface disconnected after the undo has completed. This only impacts auto-connections because manual connections whose plug or slot is missing in the new revision are kept in the connection state, but auto-connections are pruned. A similar idea applies to static plug and slot attributes from the new revision. If the refresh fails, rollback restores the old snap revision, but the new revision's static attributes are kept in the connection state.
1 parent a99cf1a commit 496d4a1

7 files changed

Lines changed: 304 additions & 18 deletions

File tree

overlord/ifacestate/handlers.go

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,9 @@ func (d delayedEffectsForSnaps) EnqueueFor(snapName affectedSnap, backend interf
337337
d[snapName][backend] = append(d[snapName][backend], item)
338338
}
339339

340+
// refreshAppSetConnections refreshes repository connections for appSet and, on
341+
// the setup-profiles do path, records undo data for persisted connection state
342+
// that reloadConnections changed or dropped.
340343
func (m *InterfaceManager) refreshAppSetConnections(task *state.Task, appSet *interfaces.SnapAppSet) ([]string, []string, error) {
341344
snapInfo := appSet.Info()
342345
snapName := appSet.InstanceName()
@@ -369,11 +372,21 @@ func (m *InterfaceManager) refreshAppSetConnections(task *state.Task, appSet *in
369372
task.Logf("%s", snap.BadInterfacesSummary(snapInfo))
370373
}
371374

372-
affectedConnections, err := m.reloadConnections(snapName)
375+
reloadedConns, changedOrDroppedConns, err := m.reloadConnections(snapName)
373376
if err != nil {
374377
return nil, nil, err
375378
}
376-
return disconnectedSnaps, affectedConnections, nil
379+
380+
// if this task modified any connection states, take a snapshot of the
381+
// original connections so that setup-profiles' undo can restore them, if
382+
// needed
383+
if task.Status() != state.UndoingStatus {
384+
if err := snapshotChangedConnectionsForUndo(task, snapName, changedOrDroppedConns); err != nil {
385+
return nil, nil, err
386+
}
387+
}
388+
389+
return disconnectedSnaps, reloadedConns, nil
377390
}
378391

379392
func (m *InterfaceManager) setupProfilesForAppSet(
@@ -385,7 +398,7 @@ func (m *InterfaceManager) setupProfilesForAppSet(
385398
st := task.State()
386399

387400
snapName := appSet.InstanceName()
388-
disconnectedSnaps, affectedConnections, err := m.refreshAppSetConnections(task, appSet)
401+
disconnectedSnaps, reloadedConns, err := m.refreshAppSetConnections(task, appSet)
389402
if err != nil {
390403
return nil, err
391404
}
@@ -399,7 +412,7 @@ func (m *InterfaceManager) setupProfilesForAppSet(
399412
snapsWithConnectedSlots := make(map[string]bool)
400413
newConnectedSnaps := make(map[string]bool)
401414
// Identify affected snaps on either side of the connection.
402-
for _, connID := range affectedConnections {
415+
for _, connID := range reloadedConns {
403416
connRef, err := interfaces.ParseConnRef(connID)
404417
if err != nil {
405418
return nil, fmt.Errorf("internal error: cannot parse existing connection: %w", err)
@@ -697,6 +710,13 @@ func (m *InterfaceManager) undoSetupProfiles(task *state.Task, tomb *tomb.Tomb)
697710
return err
698711
}
699712

713+
// restore any connection state snapshot saved by refreshAppSetConnections on
714+
// the original setup-profiles do path before rebuilding profiles for the old
715+
// revision
716+
if err := restoreConnectionsForSetupProfiles(task); err != nil {
717+
return err
718+
}
719+
700720
// Get the name from SnapSetup and use it to find the current SideInfo
701721
// about the snap, if there is one.
702722
var snapst snapstate.SnapState
@@ -2003,7 +2023,7 @@ func (m *InterfaceManager) transitionConnectionsCoreMigration(st *state.State, o
20032023
// on disk are rewritten. This is ok because core/ubuntu-core have
20042024
// exactly the same profiles and nothing in the generated policies
20052025
// has the core snap-name encoded.
2006-
if _, err := m.reloadConnections(newName); err != nil {
2026+
if _, _, err := m.reloadConnections(newName); err != nil {
20072027
return err
20082028
}
20092029

overlord/ifacestate/helpers.go

Lines changed: 102 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -426,15 +426,102 @@ func isBroken(st *state.State, snapName string) (bool, error) {
426426
return false, nil
427427
}
428428

429+
func cloneConnState(connState *schema.ConnState) *schema.ConnState {
430+
clone := *connState
431+
432+
cloneAttrs := func(attrs map[string]any) map[string]any {
433+
if attrs == nil {
434+
return nil
435+
}
436+
return utils.CopyAttributes(attrs)
437+
}
438+
439+
clone.StaticPlugAttrs = cloneAttrs(connState.StaticPlugAttrs)
440+
clone.DynamicPlugAttrs = cloneAttrs(connState.DynamicPlugAttrs)
441+
clone.StaticSlotAttrs = cloneAttrs(connState.StaticSlotAttrs)
442+
clone.DynamicSlotAttrs = cloneAttrs(connState.DynamicSlotAttrs)
443+
444+
return &clone
445+
}
446+
447+
// snapshotChangedConnectionsForUndo records original states for persisted
448+
// connections that setup-profiles changed or dropped so undo can restore them,
449+
// if needed.
450+
func snapshotChangedConnectionsForUndo(task *state.Task, instanceName string, changedConns map[string]*schema.ConnState) error {
451+
if len(changedConns) == 0 {
452+
return nil
453+
}
454+
455+
// if this isn't the setup-profiles task that is going to handle the undo,
456+
// then we don't need to keep track of these on the task
457+
if !shouldUndoSetupProfiles(task, instanceName) {
458+
return nil
459+
}
460+
461+
var connectionSnapshot map[string]*schema.ConnState
462+
err := task.Get("changed-or-dropped-connection-snapshot", &connectionSnapshot)
463+
if err != nil && !errors.Is(err, state.ErrNoState) {
464+
return err
465+
}
466+
if connectionSnapshot == nil {
467+
connectionSnapshot = make(map[string]*schema.ConnState)
468+
}
469+
470+
for connID, connState := range changedConns {
471+
if connectionSnapshot[connID] != nil {
472+
// a setup-profiles task can be retried after saving the connection
473+
// states and unlocking for backend setup. keep the first snapshot.
474+
continue
475+
}
476+
connectionSnapshot[connID] = connState
477+
}
478+
479+
task.Set("changed-or-dropped-connection-snapshot", connectionSnapshot)
480+
481+
return nil
482+
}
483+
484+
// restoreConnectionsForSetupProfiles restores connection states saved by
485+
// snapshotChangedConnectionsForUndo on a setup-profiles task.
486+
func restoreConnectionsForSetupProfiles(task *state.Task) error {
487+
var connectionSnapshot map[string]*schema.ConnState
488+
err := task.Get("changed-or-dropped-connection-snapshot", &connectionSnapshot)
489+
if errors.Is(err, state.ErrNoState) {
490+
return nil
491+
}
492+
if err != nil {
493+
return err
494+
}
495+
496+
st := task.State()
497+
498+
conns, err := getConns(st)
499+
if err != nil {
500+
return err
501+
}
502+
503+
for connID, connState := range connectionSnapshot {
504+
conns[connID] = connState
505+
}
506+
setConns(st, conns)
507+
508+
return nil
509+
}
510+
429511
// reloadConnections reloads connections stored in the state in the repository.
430512
// Using non-empty snapName the operation can be scoped to connections
431513
// affecting a given snap.
432514
//
433-
// The return value is the list of affected snap names and their connection IDs.
434-
func (m *InterfaceManager) reloadConnections(snapName string) (reloadedConnectionIDs []string, err error) {
515+
// The return value is the list of reloaded connection IDs, plus the original
516+
// connection states whose persisted state was changed or dropped.
517+
func (m *InterfaceManager) reloadConnections(snapName string) (
518+
reloadedConnectionIDs []string,
519+
changedOrDroppedConns map[string]*schema.ConnState,
520+
err error,
521+
) {
435522
conns, err := getConns(m.state)
436523
if err != nil {
437-
return nil, err
524+
return nil, nil, err
438525
}
439526

440527
var policyChecker interfaces.PolicyFunc
@@ -445,21 +532,22 @@ func (m *InterfaceManager) reloadConnections(snapName string) (reloadedConnectio
445532
if errors.Is(err, state.ErrNoState) {
446533
// everything else is a noop, as no model means no connections
447534
// to reload
448-
return nil, nil
535+
return nil, nil, nil
449536
} else if err != nil {
450-
return nil, err
537+
return nil, nil, err
451538
}
452539
autoChecker, err = newAutoConnectChecker(m.state, m.repo, deviceCtx)
453540
if err != nil {
454-
return nil, err
541+
return nil, nil, err
455542
}
456543

457544
connChecker, err = newConnectChecker(m.state, deviceCtx)
458545
if err != nil {
459-
return nil, err
546+
return nil, nil, err
460547
}
461548

462549
connStateChanged := false
550+
changedOrDroppedConns = make(map[string]*schema.ConnState)
463551

464552
var reloadedConnections []string
465553
ConnsLoop:
@@ -473,7 +561,7 @@ ConnsLoop:
473561
}
474562
connRef, err := interfaces.ParseConnRef(connId)
475563
if err != nil {
476-
return nil, err
564+
return nil, nil, err
477565
}
478566
// Apply filtering, this allows us to reload only a subset of
479567
// connections (and similarly, refresh the static attributes of only a
@@ -497,13 +585,14 @@ ConnsLoop:
497585
for _, snapName := range []string{connRef.PlugRef.Snap, connRef.SlotRef.Snap} {
498586
broken, err := isBroken(m.state, snapName)
499587
if err != nil {
500-
return nil, err
588+
return nil, nil, err
501589
}
502590
if broken {
503591
logger.Noticef("Snap %q is broken, ignored by reloadConnections", snapName)
504592
continue ConnsLoop
505593
}
506594
}
595+
changedOrDroppedConns[connId] = cloneConnState(connState)
507596
delete(conns, connId)
508597
connStateChanged = true
509598
}
@@ -540,11 +629,11 @@ ConnsLoop:
540629

541630
plugAppSet, err := interfaces.NewSnapAppSet(plugInfo.Snap, nil)
542631
if err != nil {
543-
return nil, err
632+
return nil, nil, err
544633
}
545634
slotAppSet, err := interfaces.NewSnapAppSet(slotInfo.Snap, nil)
546635
if err != nil {
547-
return nil, err
636+
return nil, nil, err
548637
}
549638

550639
cplug := interfaces.NewConnectedPlug(plugInfo, plugAppSet, newStaticPlugAttrs, connState.DynamicPlugAttrs)
@@ -568,6 +657,7 @@ ConnsLoop:
568657
reloadedConnections = append(reloadedConnections, connId)
569658

570659
if updateStaticAttrs {
660+
changedOrDroppedConns[connId] = cloneConnState(connState)
571661
connState.StaticPlugAttrs = staticPlugAttrs
572662
connState.StaticSlotAttrs = staticSlotAttrs
573663
connStateChanged = true
@@ -578,7 +668,7 @@ ConnsLoop:
578668
setConns(m.state, conns)
579669
}
580670

581-
return reloadedConnections, nil
671+
return reloadedConnections, changedOrDroppedConns, nil
582672
}
583673

584674
// removeConnections disconnects all connections of the snap in the repo. It should only be used if the snap

overlord/ifacestate/ifacemgr.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ func (m *InterfaceManager) StartUp() error {
206206
if err := removeStaleConnections(m.state); err != nil {
207207
return err
208208
}
209-
if _, err := m.reloadConnections(""); err != nil {
209+
if _, _, err := m.reloadConnections(""); err != nil {
210210
return err
211211
}
212212

0 commit comments

Comments
 (0)