github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/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 }