github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/overlord/ifacestate/ifacestate.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016-2017 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  // Package ifacestate implements the manager and state aspects
    21  // responsible for the maintenance of interfaces the system.
    22  package ifacestate
    23  
    24  import (
    25  	"fmt"
    26  	"sync"
    27  	"time"
    28  
    29  	"github.com/snapcore/snapd/asserts"
    30  	"github.com/snapcore/snapd/i18n"
    31  	"github.com/snapcore/snapd/interfaces"
    32  	"github.com/snapcore/snapd/interfaces/policy"
    33  	"github.com/snapcore/snapd/overlord/assertstate"
    34  	"github.com/snapcore/snapd/overlord/hookstate"
    35  	"github.com/snapcore/snapd/overlord/snapstate"
    36  	"github.com/snapcore/snapd/overlord/state"
    37  	"github.com/snapcore/snapd/snap"
    38  )
    39  
    40  var connectRetryTimeout = time.Second * 5
    41  
    42  // ErrAlreadyConnected describes the error that occurs when attempting to connect already connected interface.
    43  type ErrAlreadyConnected struct {
    44  	Connection interfaces.ConnRef
    45  }
    46  
    47  const (
    48  	ConnectTaskEdge       = state.TaskSetEdge("connect-task")
    49  	AfterConnectHooksEdge = state.TaskSetEdge("after-connect-hooks")
    50  )
    51  
    52  func (e ErrAlreadyConnected) Error() string {
    53  	return fmt.Sprintf("already connected: %q", e.Connection.ID())
    54  }
    55  
    56  // findSymmetricAutoconnectTask checks if there is another auto-connect task affecting same snap because of plug/slot.
    57  func findSymmetricAutoconnectTask(st *state.State, plugSnap, slotSnap string, installTask *state.Task) (bool, error) {
    58  	snapsup, err := snapstate.TaskSnapSetup(installTask)
    59  	if err != nil {
    60  		return false, fmt.Errorf("internal error: cannot obtain snap setup from task: %s", installTask.Summary())
    61  	}
    62  	installedSnap := snapsup.InstanceName()
    63  
    64  	// if we find any auto-connect task that's not ready and is affecting our snap, return true to indicate that
    65  	// it should be ignored (we shouldn't create connect tasks for it)
    66  	for _, task := range st.Tasks() {
    67  		if !task.Status().Ready() && task.ID() != installTask.ID() && task.Kind() == "auto-connect" {
    68  			snapsup, err := snapstate.TaskSnapSetup(task)
    69  			if err != nil {
    70  				return false, fmt.Errorf("internal error: cannot obtain snap setup from task: %s", task.Summary())
    71  			}
    72  			otherSnap := snapsup.InstanceName()
    73  
    74  			if (otherSnap == plugSnap && installedSnap == slotSnap) || (otherSnap == slotSnap && installedSnap == plugSnap) {
    75  				return true, nil
    76  			}
    77  		}
    78  	}
    79  	return false, nil
    80  }
    81  
    82  type connectOpts struct {
    83  	ByGadget    bool
    84  	AutoConnect bool
    85  
    86  	DelayedSetupProfiles bool
    87  }
    88  
    89  // Connect returns a set of tasks for connecting an interface.
    90  //
    91  func Connect(st *state.State, plugSnap, plugName, slotSnap, slotName string) (*state.TaskSet, error) {
    92  	if err := snapstate.CheckChangeConflictMany(st, []string{plugSnap, slotSnap}, ""); err != nil {
    93  		return nil, err
    94  	}
    95  
    96  	return connect(st, plugSnap, plugName, slotSnap, slotName, connectOpts{})
    97  }
    98  
    99  func connect(st *state.State, plugSnap, plugName, slotSnap, slotName string, flags connectOpts) (*state.TaskSet, error) {
   100  	// TODO: Store the intent-to-connect in the state so that we automatically
   101  	// try to reconnect on reboot (reconnection can fail or can connect with
   102  	// different parameters so we cannot store the actual connection details).
   103  
   104  	// Create a series of tasks:
   105  	//  - prepare-plug-<plug> hook
   106  	//  - prepare-slot-<slot> hook
   107  	//  - connect task
   108  	//  - connect-slot-<slot> hook
   109  	//  - connect-plug-<plug> hook
   110  	// The tasks run in sequence (are serialized by WaitFor). The hooks are optional
   111  	// and their tasks are created when hook exists or is declared in the snap.
   112  	// The prepare- hooks collect attributes via snapctl set.
   113  	// 'snapctl set' can only modify own attributes (plug's attributes in the *-plug-* hook and
   114  	// slot's attributes in the *-slot-* hook).
   115  	// 'snapctl get' can read both slot's and plug's attributes.
   116  
   117  	// check if the connection already exists
   118  	conns, err := getConns(st)
   119  	if err != nil {
   120  		return nil, err
   121  	}
   122  	connRef := interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: plugSnap, Name: plugName}, SlotRef: interfaces.SlotRef{Snap: slotSnap, Name: slotName}}
   123  	if conn, ok := conns[connRef.ID()]; ok && !conn.Undesired && !conn.HotplugGone {
   124  		return nil, &ErrAlreadyConnected{Connection: connRef}
   125  	}
   126  
   127  	var plugSnapst, slotSnapst snapstate.SnapState
   128  	if err = snapstate.Get(st, plugSnap, &plugSnapst); err != nil {
   129  		return nil, err
   130  	}
   131  	if err = snapstate.Get(st, slotSnap, &slotSnapst); err != nil {
   132  		return nil, err
   133  	}
   134  	plugSnapInfo, err := plugSnapst.CurrentInfo()
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  	slotSnapInfo, err := slotSnapst.CurrentInfo()
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  
   143  	plugStatic, slotStatic, err := initialConnectAttributes(st, plugSnapInfo, plugSnap, plugName, slotSnapInfo, slotSnap, slotName)
   144  	if err != nil {
   145  		return nil, err
   146  	}
   147  
   148  	connectInterface := st.NewTask("connect", fmt.Sprintf(i18n.G("Connect %s:%s to %s:%s"), plugSnap, plugName, slotSnap, slotName))
   149  	initialContext := make(map[string]interface{})
   150  	initialContext["attrs-task"] = connectInterface.ID()
   151  
   152  	tasks := state.NewTaskSet()
   153  	var prev *state.Task
   154  	addTask := func(t *state.Task) {
   155  		if prev != nil {
   156  			t.WaitFor(prev)
   157  		}
   158  		tasks.AddTask(t)
   159  	}
   160  
   161  	preparePlugHookName := fmt.Sprintf("prepare-plug-%s", plugName)
   162  	if plugSnapInfo.Hooks[preparePlugHookName] != nil {
   163  		plugHookSetup := &hookstate.HookSetup{
   164  			Snap:     plugSnap,
   165  			Hook:     preparePlugHookName,
   166  			Optional: true,
   167  		}
   168  		summary := fmt.Sprintf(i18n.G("Run hook %s of snap %q"), plugHookSetup.Hook, plugHookSetup.Snap)
   169  		undoPrepPlugHookSetup := &hookstate.HookSetup{
   170  			Snap:        plugSnap,
   171  			Hook:        "unprepare-plug-" + plugName,
   172  			Optional:    true,
   173  			IgnoreError: true,
   174  		}
   175  		preparePlugConnection := hookstate.HookTaskWithUndo(st, summary, plugHookSetup, undoPrepPlugHookSetup, initialContext)
   176  		addTask(preparePlugConnection)
   177  		prev = preparePlugConnection
   178  	}
   179  
   180  	prepareSlotHookName := fmt.Sprintf("prepare-slot-%s", slotName)
   181  	if slotSnapInfo.Hooks[prepareSlotHookName] != nil {
   182  		slotHookSetup := &hookstate.HookSetup{
   183  			Snap:     slotSnap,
   184  			Hook:     prepareSlotHookName,
   185  			Optional: true,
   186  		}
   187  		undoPrepSlotHookSetup := &hookstate.HookSetup{
   188  			Snap:        slotSnap,
   189  			Hook:        "unprepare-slot-" + slotName,
   190  			Optional:    true,
   191  			IgnoreError: true,
   192  		}
   193  
   194  		summary := fmt.Sprintf(i18n.G("Run hook %s of snap %q"), slotHookSetup.Hook, slotHookSetup.Snap)
   195  		prepareSlotConnection := hookstate.HookTaskWithUndo(st, summary, slotHookSetup, undoPrepSlotHookSetup, initialContext)
   196  		addTask(prepareSlotConnection)
   197  		prev = prepareSlotConnection
   198  	}
   199  
   200  	connectInterface.Set("slot", interfaces.SlotRef{Snap: slotSnap, Name: slotName})
   201  	connectInterface.Set("plug", interfaces.PlugRef{Snap: plugSnap, Name: plugName})
   202  	if flags.AutoConnect {
   203  		connectInterface.Set("auto", true)
   204  	}
   205  	if flags.ByGadget {
   206  		connectInterface.Set("by-gadget", true)
   207  	}
   208  	if flags.DelayedSetupProfiles {
   209  		connectInterface.Set("delayed-setup-profiles", true)
   210  	}
   211  
   212  	// Expose a copy of all plug and slot attributes coming from yaml to interface hooks. The hooks will be able
   213  	// to modify them but all attributes will be checked against assertions after the hooks are run.
   214  	emptyDynamicAttrs := map[string]interface{}{}
   215  	connectInterface.Set("plug-static", plugStatic)
   216  	connectInterface.Set("slot-static", slotStatic)
   217  	connectInterface.Set("plug-dynamic", emptyDynamicAttrs)
   218  	connectInterface.Set("slot-dynamic", emptyDynamicAttrs)
   219  
   220  	// The main 'connect' task should wait on prepare-slot- hook or on prepare-plug- hook (whichever is present),
   221  	// but not on both. While there would be no harm in waiting for both, it's not needed as prepare-slot- will
   222  	// wait for prepare-plug- anyway, and a simple one-to-one wait dependency makes testing easier.
   223  	addTask(connectInterface)
   224  	prev = connectInterface
   225  
   226  	if flags.DelayedSetupProfiles {
   227  		// mark as the last task in connect prepare
   228  		tasks.MarkEdge(connectInterface, ConnectTaskEdge)
   229  	}
   230  
   231  	connectSlotHookName := fmt.Sprintf("connect-slot-%s", slotName)
   232  	if slotSnapInfo.Hooks[connectSlotHookName] != nil {
   233  		connectSlotHookSetup := &hookstate.HookSetup{
   234  			Snap:     slotSnap,
   235  			Hook:     connectSlotHookName,
   236  			Optional: true,
   237  		}
   238  		undoConnectSlotHookSetup := &hookstate.HookSetup{
   239  			Snap:        slotSnap,
   240  			Hook:        "disconnect-slot-" + slotName,
   241  			Optional:    true,
   242  			IgnoreError: true,
   243  		}
   244  
   245  		summary := fmt.Sprintf(i18n.G("Run hook %s of snap %q"), connectSlotHookSetup.Hook, connectSlotHookSetup.Snap)
   246  		connectSlotConnection := hookstate.HookTaskWithUndo(st, summary, connectSlotHookSetup, undoConnectSlotHookSetup, initialContext)
   247  		addTask(connectSlotConnection)
   248  		prev = connectSlotConnection
   249  		if flags.DelayedSetupProfiles {
   250  			tasks.MarkEdge(connectSlotConnection, AfterConnectHooksEdge)
   251  		}
   252  	}
   253  
   254  	connectPlugHookName := fmt.Sprintf("connect-plug-%s", plugName)
   255  	if plugSnapInfo.Hooks[connectPlugHookName] != nil {
   256  		connectPlugHookSetup := &hookstate.HookSetup{
   257  			Snap:     plugSnap,
   258  			Hook:     connectPlugHookName,
   259  			Optional: true,
   260  		}
   261  		undoConnectPlugHookSetup := &hookstate.HookSetup{
   262  			Snap:        plugSnap,
   263  			Hook:        "disconnect-plug-" + plugName,
   264  			Optional:    true,
   265  			IgnoreError: true,
   266  		}
   267  
   268  		summary := fmt.Sprintf(i18n.G("Run hook %s of snap %q"), connectPlugHookSetup.Hook, connectPlugHookSetup.Snap)
   269  		connectPlugConnection := hookstate.HookTaskWithUndo(st, summary, connectPlugHookSetup, undoConnectPlugHookSetup, initialContext)
   270  		addTask(connectPlugConnection)
   271  
   272  		if flags.DelayedSetupProfiles {
   273  			// only mark AfterConnectHooksEdge if not already set on connect-slot- hook task
   274  			if edge, _ := tasks.Edge(AfterConnectHooksEdge); edge == nil {
   275  				tasks.MarkEdge(connectPlugConnection, AfterConnectHooksEdge)
   276  			}
   277  		}
   278  		prev = connectPlugConnection
   279  	}
   280  	return tasks, nil
   281  }
   282  
   283  func initialConnectAttributes(st *state.State, plugSnapInfo *snap.Info, plugSnap string, plugName string, slotSnapInfo *snap.Info, slotSnap string, slotName string) (plugStatic, slotStatic map[string]interface{}, err error) {
   284  	var plugSnapst snapstate.SnapState
   285  
   286  	if err = snapstate.Get(st, plugSnap, &plugSnapst); err != nil {
   287  		return nil, nil, err
   288  	}
   289  
   290  	plug, ok := plugSnapInfo.Plugs[plugName]
   291  	if !ok {
   292  		return nil, nil, fmt.Errorf("snap %q has no plug named %q", plugSnap, plugName)
   293  	}
   294  
   295  	var slotSnapst snapstate.SnapState
   296  
   297  	if err = snapstate.Get(st, slotSnap, &slotSnapst); err != nil {
   298  		return nil, nil, err
   299  	}
   300  
   301  	if err := addImplicitSlots(st, slotSnapInfo); err != nil {
   302  		return nil, nil, err
   303  	}
   304  
   305  	slot, ok := slotSnapInfo.Slots[slotName]
   306  	if !ok {
   307  		return nil, nil, fmt.Errorf("snap %q has no slot named %q", slotSnap, slotName)
   308  	}
   309  
   310  	return plug.Attrs, slot.Attrs, nil
   311  }
   312  
   313  // Disconnect returns a set of tasks for disconnecting an interface.
   314  func Disconnect(st *state.State, conn *interfaces.Connection) (*state.TaskSet, error) {
   315  	plugSnap := conn.Plug.Snap().InstanceName()
   316  	slotSnap := conn.Slot.Snap().InstanceName()
   317  	if err := snapstate.CheckChangeConflictMany(st, []string{plugSnap, slotSnap}, ""); err != nil {
   318  		return nil, err
   319  	}
   320  
   321  	return disconnectTasks(st, conn, disconnectOpts{})
   322  }
   323  
   324  // Forget returs a set of tasks for disconnecting and forgetting an interface.
   325  // If the interface is already disconnected, it will be removed from the state
   326  // (forgotten).
   327  func Forget(st *state.State, repo *interfaces.Repository, connRef *interfaces.ConnRef) (*state.TaskSet, error) {
   328  	if err := snapstate.CheckChangeConflictMany(st, []string{connRef.PlugRef.Snap, connRef.SlotRef.Snap}, ""); err != nil {
   329  		return nil, err
   330  	}
   331  
   332  	if conn, err := repo.Connection(connRef); err == nil {
   333  		// connection exists - run regular set of disconnect tasks with forget
   334  		// flag.
   335  		opts := disconnectOpts{Forget: true}
   336  		ts, err := disconnectTasks(st, conn, opts)
   337  		return ts, err
   338  	}
   339  
   340  	// connection is not active (and possibly either the plug or slot
   341  	// doesn't exist); disconnect tasks don't need hooks as we simply
   342  	// want to remove connection from state.
   343  	ts := forgetTasks(st, connRef)
   344  	return ts, nil
   345  }
   346  
   347  type disconnectOpts struct {
   348  	AutoDisconnect bool
   349  	ByHotplug      bool
   350  	Forget         bool
   351  }
   352  
   353  // forgetTasks creates a set of tasks for forgetting an inactive connection
   354  func forgetTasks(st *state.State, connRef *interfaces.ConnRef) *state.TaskSet {
   355  	summary := fmt.Sprintf(i18n.G("Forget connection %s:%s from %s:%s"),
   356  		connRef.PlugRef.Snap, connRef.PlugRef.Name,
   357  		connRef.SlotRef.Snap, connRef.SlotRef.Name)
   358  	disconnectTask := st.NewTask("disconnect", summary)
   359  	disconnectTask.Set("slot", connRef.SlotRef)
   360  	disconnectTask.Set("plug", connRef.PlugRef)
   361  	disconnectTask.Set("forget", true)
   362  	return state.NewTaskSet(disconnectTask)
   363  }
   364  
   365  // disconnectTasks creates a set of tasks for disconnect, including hooks, but does not do any conflict checking.
   366  func disconnectTasks(st *state.State, conn *interfaces.Connection, flags disconnectOpts) (*state.TaskSet, error) {
   367  	plugSnap := conn.Plug.Snap().InstanceName()
   368  	slotSnap := conn.Slot.Snap().InstanceName()
   369  	plugName := conn.Plug.Name()
   370  	slotName := conn.Slot.Name()
   371  
   372  	var plugSnapst, slotSnapst snapstate.SnapState
   373  	if err := snapstate.Get(st, slotSnap, &slotSnapst); err != nil {
   374  		return nil, err
   375  	}
   376  	if err := snapstate.Get(st, plugSnap, &plugSnapst); err != nil {
   377  		return nil, err
   378  	}
   379  
   380  	summary := fmt.Sprintf(i18n.G("Disconnect %s:%s from %s:%s"),
   381  		plugSnap, plugName, slotSnap, slotName)
   382  	disconnectTask := st.NewTask("disconnect", summary)
   383  	disconnectTask.Set("slot", interfaces.SlotRef{Snap: slotSnap, Name: slotName})
   384  	disconnectTask.Set("plug", interfaces.PlugRef{Snap: plugSnap, Name: plugName})
   385  	if flags.Forget {
   386  		disconnectTask.Set("forget", true)
   387  	}
   388  
   389  	disconnectTask.Set("slot-static", conn.Slot.StaticAttrs())
   390  	disconnectTask.Set("slot-dynamic", conn.Slot.DynamicAttrs())
   391  	disconnectTask.Set("plug-static", conn.Plug.StaticAttrs())
   392  	disconnectTask.Set("plug-dynamic", conn.Plug.DynamicAttrs())
   393  
   394  	if flags.AutoDisconnect {
   395  		disconnectTask.Set("auto-disconnect", true)
   396  	}
   397  	if flags.ByHotplug {
   398  		disconnectTask.Set("by-hotplug", true)
   399  	}
   400  
   401  	ts := state.NewTaskSet()
   402  	var prev *state.Task
   403  	addTask := func(t *state.Task) {
   404  		if prev != nil {
   405  			t.WaitFor(prev)
   406  		}
   407  		ts.AddTask(t)
   408  		prev = t
   409  	}
   410  
   411  	initialContext := make(map[string]interface{})
   412  	initialContext["attrs-task"] = disconnectTask.ID()
   413  
   414  	plugSnapInfo, err := plugSnapst.CurrentInfo()
   415  	if err != nil {
   416  		return nil, err
   417  	}
   418  	slotSnapInfo, err := slotSnapst.CurrentInfo()
   419  	if err != nil {
   420  		return nil, err
   421  	}
   422  
   423  	// only run slot hooks if slotSnap is active
   424  	if slotSnapst.Active {
   425  		hookName := fmt.Sprintf("disconnect-slot-%s", slotName)
   426  		if slotSnapInfo.Hooks[hookName] != nil {
   427  			disconnectSlotHookSetup := &hookstate.HookSetup{
   428  				Snap:     slotSnap,
   429  				Hook:     hookName,
   430  				Optional: true,
   431  			}
   432  			undoDisconnectSlotHookSetup := &hookstate.HookSetup{
   433  				Snap:     slotSnap,
   434  				Hook:     "connect-slot-" + slotName,
   435  				Optional: true,
   436  			}
   437  
   438  			summary := fmt.Sprintf(i18n.G("Run hook %s of snap %q"), disconnectSlotHookSetup.Hook, disconnectSlotHookSetup.Snap)
   439  			disconnectSlot := hookstate.HookTaskWithUndo(st, summary, disconnectSlotHookSetup, undoDisconnectSlotHookSetup, initialContext)
   440  
   441  			addTask(disconnectSlot)
   442  		}
   443  	}
   444  
   445  	// only run plug hooks if plugSnap is active
   446  	if plugSnapst.Active {
   447  		hookName := fmt.Sprintf("disconnect-plug-%s", plugName)
   448  		if plugSnapInfo.Hooks[hookName] != nil {
   449  			disconnectPlugHookSetup := &hookstate.HookSetup{
   450  				Snap:     plugSnap,
   451  				Hook:     hookName,
   452  				Optional: true,
   453  			}
   454  			undoDisconnectPlugHookSetup := &hookstate.HookSetup{
   455  				Snap:     plugSnap,
   456  				Hook:     "connect-plug-" + plugName,
   457  				Optional: true,
   458  			}
   459  
   460  			summary := fmt.Sprintf(i18n.G("Run hook %s of snap %q"), disconnectPlugHookSetup.Hook, disconnectPlugHookSetup.Snap)
   461  			disconnectPlug := hookstate.HookTaskWithUndo(st, summary, disconnectPlugHookSetup, undoDisconnectPlugHookSetup, initialContext)
   462  
   463  			addTask(disconnectPlug)
   464  		}
   465  	}
   466  
   467  	addTask(disconnectTask)
   468  	return ts, nil
   469  }
   470  
   471  // CheckInterfaces checks whether plugs and slots of snap are allowed for installation.
   472  func CheckInterfaces(st *state.State, snapInfo *snap.Info, deviceCtx snapstate.DeviceContext) error {
   473  	// XXX: addImplicitSlots is really a brittle interface
   474  	if err := addImplicitSlots(st, snapInfo); err != nil {
   475  		return err
   476  	}
   477  
   478  	modelAs := deviceCtx.Model()
   479  
   480  	var storeAs *asserts.Store
   481  	if modelAs.Store() != "" {
   482  		var err error
   483  		storeAs, err = assertstate.Store(st, modelAs.Store())
   484  		if err != nil && !asserts.IsNotFound(err) {
   485  			return err
   486  		}
   487  	}
   488  
   489  	baseDecl, err := assertstate.BaseDeclaration(st)
   490  	if err != nil {
   491  		return fmt.Errorf("internal error: cannot find base declaration: %v", err)
   492  	}
   493  
   494  	if snapInfo.SnapID == "" {
   495  		// no SnapID means --dangerous was given, perform a minimal check about the compatibility of the snap type and the interface
   496  		ic := policy.InstallCandidateMinimalCheck{
   497  			Snap:            snapInfo,
   498  			BaseDeclaration: baseDecl,
   499  			Model:           modelAs,
   500  			Store:           storeAs,
   501  		}
   502  		return ic.Check()
   503  	}
   504  
   505  	snapDecl, err := assertstate.SnapDeclaration(st, snapInfo.SnapID)
   506  	if err != nil {
   507  		return fmt.Errorf("cannot find snap declaration for %q: %v", snapInfo.InstanceName(), err)
   508  	}
   509  
   510  	ic := policy.InstallCandidate{
   511  		Snap:            snapInfo,
   512  		SnapDeclaration: snapDecl,
   513  		BaseDeclaration: baseDecl,
   514  		Model:           modelAs,
   515  		Store:           storeAs,
   516  	}
   517  
   518  	return ic.Check()
   519  }
   520  
   521  var once sync.Once
   522  
   523  func delayedCrossMgrInit() {
   524  	once.Do(func() {
   525  		// hook interface checks into snapstate installation logic
   526  
   527  		snapstate.AddCheckSnapCallback(func(st *state.State, snapInfo, _ *snap.Info, _ snap.Container, _ snapstate.Flags, deviceCtx snapstate.DeviceContext) error {
   528  			return CheckInterfaces(st, snapInfo, deviceCtx)
   529  		})
   530  
   531  		// hook into conflict checks mechanisms
   532  		snapstate.AddAffectedSnapsByKind("connect", connectDisconnectAffectedSnaps)
   533  		snapstate.AddAffectedSnapsByKind("disconnect", connectDisconnectAffectedSnaps)
   534  	})
   535  }
   536  
   537  func MockConnectRetryTimeout(d time.Duration) (restore func()) {
   538  	old := connectRetryTimeout
   539  	connectRetryTimeout = d
   540  	return func() { connectRetryTimeout = old }
   541  }