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

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2018 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
    21  
    22  import (
    23  	"crypto/sha256"
    24  	"fmt"
    25  	"strings"
    26  	"unicode"
    27  
    28  	"github.com/snapcore/snapd/features"
    29  	"github.com/snapcore/snapd/interfaces"
    30  	"github.com/snapcore/snapd/interfaces/hotplug"
    31  	"github.com/snapcore/snapd/logger"
    32  	"github.com/snapcore/snapd/overlord/configstate/config"
    33  	"github.com/snapcore/snapd/overlord/snapstate"
    34  	"github.com/snapcore/snapd/overlord/state"
    35  	"github.com/snapcore/snapd/snap"
    36  )
    37  
    38  // deviceKey determines a key for given device and hotplug interface. Every interface may provide a custom HotplugDeviceKey method
    39  // to compute device key - if it doesn't, we fall back to defaultDeviceKey.
    40  func deviceKey(device *hotplug.HotplugDeviceInfo, iface interfaces.Interface, defaultDeviceKey snap.HotplugKey) (deviceKey snap.HotplugKey, err error) {
    41  	if keyhandler, ok := iface.(hotplug.HotplugKeyHandler); ok {
    42  		deviceKey, err = keyhandler.HotplugKey(device)
    43  		if err != nil {
    44  			return "", fmt.Errorf("cannot create hotplug key for interface %q: %s", iface.Name(), err)
    45  		}
    46  		if deviceKey != "" {
    47  			return deviceKey, nil
    48  		}
    49  	}
    50  	return defaultDeviceKey, nil
    51  }
    52  
    53  // List of attributes that determine the computation of default device key.
    54  // Attributes are grouped by similarity, the first non-empty attribute within the group goes into the key.
    55  // The final key is composed of 4 attributes (some of which may be empty), separated by "/".
    56  // Warning, any future changes to these definitions require a new key version.
    57  var attrGroups = [][][]string{
    58  	// key version 0
    59  	{
    60  		// Name
    61  		{"ID_V4L_PRODUCT", "NAME", "ID_NET_NAME", "PCI_SLOT_NAME"},
    62  		// Vendor
    63  		{"ID_VENDOR_ID", "ID_VENDOR", "ID_WWN", "ID_WWN_WITH_EXTENSION", "ID_VENDOR_FROM_DATABASE", "ID_VENDOR_ENC", "ID_OUI_FROM_DATABASE"},
    64  		// Model
    65  		{"ID_MODEL_ID", "ID_MODEL_ENC"},
    66  		// Identifier
    67  		{"ID_SERIAL", "ID_SERIAL_SHORT", "ID_NET_NAME_MAC", "ID_REVISION"},
    68  	},
    69  }
    70  
    71  // deviceKeyVersion is the current version number for the default keys computed by hotplug subsystem.
    72  // Fresh device keys always use current version format
    73  var deviceKeyVersion = len(attrGroups) - 1
    74  
    75  // defaultDeviceKey computes device key from the attributes of
    76  // HotplugDeviceInfo. Empty string is returned if too few attributes are present
    77  // to compute a good key. Attributes used to compute device key are defined in
    78  // attrGroups list above and they depend on the keyVersion passed to the
    79  // function.
    80  // The resulting key returned by the function has the following format:
    81  // <version><checksum> where checksum is the sha256 checksum computed over
    82  // select attributes of the device.
    83  func defaultDeviceKey(devinfo *hotplug.HotplugDeviceInfo, keyVersion int) (snap.HotplugKey, error) {
    84  	found := 0
    85  	key := sha256.New()
    86  	if keyVersion >= 16 || keyVersion >= len(attrGroups) {
    87  		return "", fmt.Errorf("internal error: invalid key version %d", keyVersion)
    88  	}
    89  	for _, group := range attrGroups[keyVersion] {
    90  		for _, attr := range group {
    91  			if val, ok := devinfo.Attribute(attr); ok && val != "" {
    92  				key.Write([]byte(attr))
    93  				key.Write([]byte{0})
    94  				key.Write([]byte(val))
    95  				key.Write([]byte{0})
    96  				found++
    97  				break
    98  			}
    99  		}
   100  	}
   101  	if found < 2 {
   102  		return "", nil
   103  	}
   104  	return snap.HotplugKey(fmt.Sprintf("%x%x", keyVersion, key.Sum(nil))), nil
   105  }
   106  
   107  // hotplugDeviceAdded gets called when a device is added to the system.
   108  func (m *InterfaceManager) hotplugDeviceAdded(devinfo *hotplug.HotplugDeviceInfo) {
   109  	st := m.state
   110  	st.Lock()
   111  	defer st.Unlock()
   112  
   113  	if _, err := systemSnapInfo(st); err != nil {
   114  		logger.Noticef("system snap not available, hotplug events ignored")
   115  		return
   116  	}
   117  
   118  	defaultKey, err := defaultDeviceKey(devinfo, deviceKeyVersion)
   119  	if err != nil {
   120  		logger.Noticef("cannot compute default hotplug key for device %s: %v", devinfo, err.Error())
   121  	}
   122  
   123  	hotplugFeature, err := m.hotplugEnabled()
   124  	if err != nil {
   125  		logger.Noticef("internal error: cannot get hotplug feature flag: %v", err.Error())
   126  		return
   127  	}
   128  
   129  	deviceCtx, err := snapstate.DeviceCtxFromState(st, nil)
   130  	if err != nil {
   131  		logger.Noticef("internal error: cannot get global device context: %v", err)
   132  		return
   133  	}
   134  
   135  	gadget, err := snapstate.GadgetInfo(st, deviceCtx)
   136  	if err != nil && err != state.ErrNoState {
   137  		logger.Noticef("internal error: cannot get gadget information: %v", err)
   138  	}
   139  
   140  	hotplugIfaces := m.repo.AllHotplugInterfaces()
   141  	gadgetSlotsByInterface := make(map[string][]*snap.SlotInfo)
   142  	if gadget != nil {
   143  		for _, gadgetSlot := range gadget.Slots {
   144  			if _, ok := hotplugIfaces[gadgetSlot.Interface]; ok {
   145  				gadgetSlotsByInterface[gadgetSlot.Interface] = append(gadgetSlotsByInterface[gadgetSlot.Interface], gadgetSlot)
   146  			}
   147  		}
   148  	}
   149  
   150  InterfacesLoop:
   151  	// iterate over all hotplug interfaces
   152  	for _, iface := range hotplugIfaces {
   153  		hotplugHandler := iface.(hotplug.Definer)
   154  
   155  		// ignore device that is already handled by a gadget slot
   156  		if gadgetSlots, ok := gadgetSlotsByInterface[iface.Name()]; ok {
   157  			for _, gslot := range gadgetSlots {
   158  				if pred, ok := iface.(hotplug.HandledByGadgetPredicate); ok {
   159  					if pred.HandledByGadget(devinfo, gslot) {
   160  						logger.Debugf("ignoring device %s, interface %q (handled by gadget slot %s)", devinfo, iface.Name(), gslot.Name)
   161  						continue InterfacesLoop
   162  					}
   163  				}
   164  			}
   165  		}
   166  
   167  		proposedSlot, err := hotplugHandler.HotplugDeviceDetected(devinfo)
   168  		if err != nil {
   169  			logger.Noticef("cannot process hotplug event by the rule of interface %q: %s", iface.Name(), err)
   170  			continue
   171  		}
   172  		// if the interface doesn't propose a slot, carry on and go to the next interface
   173  		if proposedSlot == nil {
   174  			continue
   175  		}
   176  
   177  		// Check the key when we know the interface wants to create a hotplug slot, doing this earlier would generate too much log noise about irrelevant devices
   178  		key, err := deviceKey(devinfo, iface, defaultKey)
   179  		if err != nil {
   180  			logger.Noticef("internal error: cannot compute hotplug key for device %s: %v", devinfo, err.Error())
   181  			continue
   182  		}
   183  		if key == "" {
   184  			logger.Noticef("no valid hotplug key provided by interface %q, device %s ignored", iface.Name(), devinfo)
   185  			continue
   186  		}
   187  
   188  		proposedSlot, err = proposedSlot.Clean()
   189  		if err != nil {
   190  			logger.Noticef("cannot validate hotplug slot proposed by interface %q for device %s: %v", iface.Name(), devinfo, err.Error())
   191  			continue
   192  		}
   193  		if proposedSlot.Label == "" {
   194  			si := interfaces.StaticInfoOf(iface)
   195  			proposedSlot.Label = si.Summary
   196  		}
   197  
   198  		if !hotplugFeature {
   199  			logger.Noticef("hotplug device add event ignored, enable experimental.hotplug")
   200  			return
   201  		}
   202  
   203  		logger.Debugf("adding hotplug device %s for interface %q, hotplug key %q", devinfo, iface.Name(), key)
   204  
   205  		seq, err := allocHotplugSeq(st)
   206  		if err != nil {
   207  			logger.Noticef("internal error: cannot handle hotplug device %s: %v", devinfo, err)
   208  			continue
   209  		}
   210  
   211  		if !m.enumerationDone {
   212  			if m.enumeratedDeviceKeys[iface.Name()] == nil {
   213  				m.enumeratedDeviceKeys[iface.Name()] = make(map[snap.HotplugKey]bool)
   214  			}
   215  			m.enumeratedDeviceKeys[iface.Name()][key] = true
   216  		}
   217  		devPath := devinfo.DevicePath()
   218  		// We may have different interfaces at same paths (e.g. a "foo-observe" and "foo-control" interfaces), therefore use lists.
   219  		// Duplicates are not expected here because if a device is plugged twice, there will be an udev "remove" event between the adds
   220  		// and hotplugDeviceRemoved() will remove affected path from hotplugDevicePaths.
   221  		m.hotplugDevicePaths[devPath] = append(m.hotplugDevicePaths[devPath], deviceData{hotplugKey: key, ifaceName: iface.Name()})
   222  
   223  		hotplugAdd := st.NewTask("hotplug-add-slot", fmt.Sprintf("Create slot for device %s with hotplug key %q", devinfo.ShortString(), key.ShortString()))
   224  		setHotplugAttrs(hotplugAdd, iface.Name(), key)
   225  		hotplugAdd.Set("device-info", devinfo)
   226  		hotplugAdd.Set("proposed-slot", proposedSlot)
   227  
   228  		hotplugConnect := st.NewTask("hotplug-connect", fmt.Sprintf("Recreate connections of interface %q for device %s with hotplug key %q", iface.Name(), devinfo.ShortString(), key.ShortString()))
   229  		setHotplugAttrs(hotplugConnect, iface.Name(), key)
   230  		hotplugConnect.WaitFor(hotplugAdd)
   231  
   232  		chg := st.NewChange(fmt.Sprintf("hotplug-add-slot-%s", iface), fmt.Sprintf("Add hotplug slot of interface %q for device %s with hotplug key %q", devinfo.ShortString(), iface.Name(), key.ShortString()))
   233  		chg.AddTask(hotplugAdd)
   234  		chg.AddTask(hotplugConnect)
   235  		addHotplugSeqWaitTask(chg, key, seq)
   236  
   237  		st.EnsureBefore(0)
   238  	}
   239  }
   240  
   241  // hotplugDeviceRemoved gets called when a device is removed from the system.
   242  func (m *InterfaceManager) hotplugDeviceRemoved(devinfo *hotplug.HotplugDeviceInfo) {
   243  	st := m.state
   244  	st.Lock()
   245  	defer st.Unlock()
   246  
   247  	hotplugFeature, err := m.hotplugEnabled()
   248  	if err != nil {
   249  		logger.Noticef("internal error: cannot get hotplug feature flag: %s", err.Error())
   250  		return
   251  	}
   252  
   253  	devPath := devinfo.DevicePath()
   254  	devs := m.hotplugDevicePaths[devPath]
   255  	delete(m.hotplugDevicePaths, devPath)
   256  
   257  	var changed bool
   258  	for _, dev := range devs {
   259  		hotplugKey := dev.hotplugKey
   260  		ifaceName := dev.ifaceName
   261  		slot, err := m.repo.SlotForHotplugKey(ifaceName, hotplugKey)
   262  		if err != nil {
   263  			logger.Noticef("internal error: cannot obtain slot for hotplug interface %q, hotplug key %q: %v", ifaceName, hotplugKey, err)
   264  			continue
   265  		}
   266  		if slot == nil {
   267  			continue
   268  		}
   269  
   270  		if !hotplugFeature {
   271  			logger.Noticef("hotplug device remove event ignored, enable experimental.hotplug")
   272  			return
   273  		}
   274  
   275  		logger.Debugf("removing hotplug device %s for interface %q, hotplug key %q", devinfo, ifaceName, hotplugKey)
   276  
   277  		seq, err := allocHotplugSeq(st)
   278  		if err != nil {
   279  			logger.Noticef("internal error: cannot handle removal of hotplug device %s, hotplug key %q: %v", devinfo, hotplugKey, err)
   280  			continue
   281  		}
   282  
   283  		ts := removeDevice(st, ifaceName, hotplugKey)
   284  		chg := st.NewChange(fmt.Sprintf("hotplug-remove-%s", ifaceName), fmt.Sprintf("Remove hotplug connections and slots of device %s with interface %q", devinfo.ShortString(), ifaceName))
   285  		chg.AddAll(ts)
   286  		addHotplugSeqWaitTask(chg, hotplugKey, seq)
   287  		changed = true
   288  	}
   289  
   290  	if changed {
   291  		st.EnsureBefore(0)
   292  	}
   293  }
   294  
   295  // hotplugEnumerationDone gets called when initial enumeration on startup is finished.
   296  func (m *InterfaceManager) hotplugEnumerationDone() {
   297  	st := m.state
   298  	st.Lock()
   299  	defer st.Unlock()
   300  
   301  	hotplugSlots, err := getHotplugSlots(st)
   302  	if err != nil {
   303  		logger.Noticef("internal error obtaining hotplug slots: %v", err.Error())
   304  		return
   305  	}
   306  
   307  	for _, slot := range hotplugSlots {
   308  		if byIface, ok := m.enumeratedDeviceKeys[slot.Interface]; ok {
   309  			if byIface[slot.HotplugKey] {
   310  				continue
   311  			}
   312  		}
   313  		// device not present, disconnect its slots and remove them (as if it was unplugged)
   314  		seq, err := allocHotplugSeq(st)
   315  		if err != nil {
   316  			logger.Noticef("internal error: cannot handle removal of hotplug slot %q: %v", slot.Name, err)
   317  			continue
   318  		}
   319  		ts := removeDevice(st, slot.Interface, slot.HotplugKey)
   320  		chg := st.NewChange(fmt.Sprintf("hotplug-remove-%s", slot.Interface), fmt.Sprintf("Remove hotplug connections and slots of interface %q", slot.Interface))
   321  		chg.AddAll(ts)
   322  		addHotplugSeqWaitTask(chg, slot.HotplugKey, seq)
   323  	}
   324  	st.EnsureBefore(0)
   325  
   326  	// the map of enumeratedDeviceKeys is not needed anymore
   327  	m.enumeratedDeviceKeys = nil
   328  	m.enumerationDone = true
   329  }
   330  
   331  func (m *InterfaceManager) hotplugEnabled() (bool, error) {
   332  	tr := config.NewTransaction(m.state)
   333  	return features.Flag(tr, features.Hotplug)
   334  }
   335  
   336  // ensureUniqueName modifies proposedName so that it's unique according to isUnique predicate.
   337  // Uniqueness is achieved by appending a numeric suffix.
   338  func ensureUniqueName(proposedName string, isUnique func(string) bool) string {
   339  	// if the name is unique right away, do nothing
   340  	if isUnique(proposedName) {
   341  		return proposedName
   342  	}
   343  
   344  	baseName := proposedName
   345  	suffixNumValue := 1
   346  	// increase suffix value until we have a unique name
   347  	for {
   348  		proposedName = fmt.Sprintf("%s-%d", baseName, suffixNumValue)
   349  		if isUnique(proposedName) {
   350  			return proposedName
   351  		}
   352  		suffixNumValue++
   353  	}
   354  }
   355  
   356  const maxGenerateSlotNameLen = 20
   357  
   358  // makeSlotName sanitizes a string to make it a valid slot name that
   359  // passes validation rules implemented by ValidateSlotName (see snap/validate.go):
   360  // - only lowercase letter, digits and dashes are allowed
   361  // - must start with a letter
   362  // - no double dashes, cannot end with a dash.
   363  // In addition names are truncated not to exceed maxGenerateSlotNameLen characters.
   364  func makeSlotName(s string) string {
   365  	var out []rune
   366  	// the dash flag is used to prevent consecutive dashes, and the dash in the front
   367  	dash := true
   368  	for _, c := range s {
   369  		switch {
   370  		case c == '-' && !dash:
   371  			dash = true
   372  			out = append(out, '-')
   373  		case unicode.IsLetter(c):
   374  			out = append(out, unicode.ToLower(c))
   375  			dash = false
   376  		case unicode.IsDigit(c) && len(out) > 0:
   377  			out = append(out, c)
   378  			dash = false
   379  		default:
   380  			// any other character is ignored
   381  		}
   382  		if len(out) >= maxGenerateSlotNameLen {
   383  			break
   384  		}
   385  	}
   386  	// make sure the name doesn't end with a dash
   387  	return strings.TrimRight(string(out), "-")
   388  }
   389  
   390  var nameAttrs = []string{"NAME", "ID_MODEL_FROM_DATABASE", "ID_MODEL"}
   391  
   392  // suggestedSlotName returns the shortest name derived from attributes defined
   393  // by nameAttrs, or the fallbackName if there is no known attribute to derive
   394  // name from. The name created from attributes is sanitized to ensure it's a
   395  // valid slot name. The fallbackName is typically the name of the interface.
   396  func suggestedSlotName(devinfo *hotplug.HotplugDeviceInfo, fallbackName string) string {
   397  	var shortestName string
   398  	for _, attr := range nameAttrs {
   399  		name, ok := devinfo.Attribute(attr)
   400  		if ok {
   401  			if name := makeSlotName(name); name != "" {
   402  				if shortestName == "" || len(name) < len(shortestName) {
   403  					shortestName = name
   404  				}
   405  			}
   406  		}
   407  	}
   408  	if len(shortestName) == 0 {
   409  		return fallbackName
   410  	}
   411  	return shortestName
   412  }
   413  
   414  // hotplugSlotName returns a slot name derived from slotSpecName or device attributes, or interface name, in that priority order, depending
   415  // on which information is available. The chosen name is guaranteed to be unique
   416  func hotplugSlotName(hotplugKey snap.HotplugKey, systemSnapInstanceName, slotSpecName, ifaceName string, devinfo *hotplug.HotplugDeviceInfo, repo *interfaces.Repository, stateSlots map[string]*HotplugSlotInfo) string {
   417  	proposedName := slotSpecName
   418  	if proposedName == "" {
   419  		proposedName = suggestedSlotName(devinfo, ifaceName)
   420  	}
   421  	proposedName = ensureUniqueName(proposedName, func(slotName string) bool {
   422  		if slot, ok := stateSlots[slotName]; ok {
   423  			return slot.HotplugKey == hotplugKey
   424  		}
   425  		return repo.Slot(systemSnapInstanceName, slotName) == nil
   426  	})
   427  	return proposedName
   428  }
   429  
   430  // updateDevice creates tasks to disconnect slots of given device and update the slot in the repository.
   431  func updateDevice(st *state.State, ifaceName string, hotplugKey snap.HotplugKey, newAttrs map[string]interface{}) *state.TaskSet {
   432  	hotplugDisconnect := st.NewTask("hotplug-disconnect", fmt.Sprintf("Disable connections of interface %q, hotplug key %q", ifaceName, hotplugKey.ShortString()))
   433  	setHotplugAttrs(hotplugDisconnect, ifaceName, hotplugKey)
   434  
   435  	updateSlot := st.NewTask("hotplug-update-slot", fmt.Sprintf("Update slot of interface %q, hotplug key %q", ifaceName, hotplugKey.ShortString()))
   436  	setHotplugAttrs(updateSlot, ifaceName, hotplugKey)
   437  	updateSlot.Set("slot-attrs", newAttrs)
   438  	updateSlot.WaitFor(hotplugDisconnect)
   439  
   440  	return state.NewTaskSet(hotplugDisconnect, updateSlot)
   441  }
   442  
   443  // removeDevice creates tasks to disconnect slots of given device and remove affected slots.
   444  func removeDevice(st *state.State, ifaceName string, hotplugKey snap.HotplugKey) *state.TaskSet {
   445  	// hotplug-disconnect task will create hooks and disconnect the slot
   446  	hotplugDisconnect := st.NewTask("hotplug-disconnect", fmt.Sprintf("Disable connections of interface %q, hotplug key %q", ifaceName, hotplugKey.ShortString()))
   447  	setHotplugAttrs(hotplugDisconnect, ifaceName, hotplugKey)
   448  
   449  	// hotplug-remove-slot will remove this device's slot from the repository.
   450  	removeSlot := st.NewTask("hotplug-remove-slot", fmt.Sprintf("Remove slot for interface %q, hotplug key %q", ifaceName, hotplugKey.ShortString()))
   451  	setHotplugAttrs(removeSlot, ifaceName, hotplugKey)
   452  	removeSlot.WaitFor(hotplugDisconnect)
   453  
   454  	return state.NewTaskSet(hotplugDisconnect, removeSlot)
   455  }