github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/network/netplan/netplan.go (about) 1 // Copyright 2018 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package netplan 5 6 import ( 7 "fmt" 8 "os" 9 "path" 10 "path/filepath" 11 "reflect" 12 "sort" 13 "strings" 14 "time" 15 16 "github.com/juju/errors" 17 goyaml "gopkg.in/yaml.v2" 18 ) 19 20 // Representation of netplan YAML format as Go structures 21 // The order of fields is consistent with Netplan docs 22 type Nameservers struct { 23 Search []string `yaml:"search,omitempty,flow"` 24 Addresses []string `yaml:"addresses,omitempty,flow"` 25 } 26 27 // Interface includes all the fields that are common between all interfaces (ethernet, wifi, bridge, bond) 28 type Interface struct { 29 AcceptRA *bool `yaml:"accept-ra,omitempty"` 30 Addresses []string `yaml:"addresses,omitempty"` 31 // Critical doesn't have to be *bool because it is only used if True 32 Critical bool `yaml:"critical,omitempty"` 33 // DHCP4 defaults to true, so we must use a pointer to know if it was specified as false 34 DHCP4 *bool `yaml:"dhcp4,omitempty"` 35 DHCP6 *bool `yaml:"dhcp6,omitempty"` 36 DHCPIdentifier string `yaml:"dhcp-identifier,omitempty"` // "duid" or "mac" 37 Gateway4 string `yaml:"gateway4,omitempty"` 38 Gateway6 string `yaml:"gateway6,omitempty"` 39 Nameservers Nameservers `yaml:"nameservers,omitempty"` 40 MACAddress string `yaml:"macaddress,omitempty"` 41 MTU int `yaml:"mtu,omitempty"` 42 Renderer string `yaml:"renderer,omitempty"` // NetworkManager or networkd 43 Routes []Route `yaml:"routes,omitempty"` 44 RoutingPolicy []RoutePolicy `yaml:"routing-policy,omitempty"` 45 // Optional doesn't have to be *bool because it is only used if True 46 Optional bool `yaml:"optional,omitempty"` 47 48 // Configure the link-local addresses to bring up. Valid options are 49 // "ipv4" and "ipv6". According to the netplan reference, netplan will 50 // only bring up ipv6 addresses if *no* link-local attribute is 51 // specified. On the other hand, if an empty link-local attribute is 52 // specified, this instructs netplan not to bring any ipv4/ipv6 address 53 // up. 54 LinkLocal *[]string `yaml:"link-local,omitempty"` 55 } 56 57 // Ethernet defines fields for just Ethernet devices 58 type Ethernet struct { 59 Match map[string]string `yaml:"match,omitempty"` 60 Wakeonlan bool `yaml:"wakeonlan,omitempty"` 61 SetName string `yaml:"set-name,omitempty"` 62 Interface `yaml:",inline"` 63 } 64 type AccessPoint struct { 65 Password string `yaml:"password,omitempty"` 66 Mode string `yaml:"mode,omitempty"` 67 Channel int `yaml:"channel,omitempty"` 68 } 69 type Wifi struct { 70 Match map[string]string `yaml:"match,omitempty"` 71 SetName string `yaml:"set-name,omitempty"` 72 Wakeonlan bool `yaml:"wakeonlan,omitempty"` 73 AccessPoints map[string]AccessPoint `yaml:"access-points,omitempty"` 74 Interface `yaml:",inline"` 75 } 76 77 type BridgeParameters struct { 78 AgeingTime *int `yaml:"ageing-time,omitempty"` 79 ForwardDelay *int `yaml:"forward-delay,omitempty"` 80 HelloTime *int `yaml:"hello-time,omitempty"` 81 MaxAge *int `yaml:"max-age,omitempty"` 82 PathCost map[string]int `yaml:"path-cost,omitempty"` 83 PortPriority map[string]int `yaml:"port-priority,omitempty"` 84 Priority *int `yaml:"priority,omitempty"` 85 STP *bool `yaml:"stp,omitempty"` 86 } 87 88 type Bridge struct { 89 Interfaces []string `yaml:"interfaces,omitempty,flow"` 90 Interface `yaml:",inline"` 91 Parameters BridgeParameters `yaml:"parameters,omitempty"` 92 93 // According to the netplan examples, this section typically includes 94 // some OVS-specific configuration bits. However, MAAS may just 95 // include an empty block to indicate the presence of an OVS-managed 96 // bridge (LP1942328). As a workaround, we make this an optional map 97 // so we can tell whether it is present (but empty) vs not being 98 // present. 99 // 100 // See: https://github.com/canonical/netplan/blob/main/examples/openvswitch.yaml 101 OVSParameters *map[string]interface{} `yaml:"openvswitch,omitempty"` 102 } 103 104 type Route struct { 105 From string `yaml:"from,omitempty"` 106 OnLink *bool `yaml:"on-link,omitempty"` 107 Scope string `yaml:"scope,omitempty"` 108 Table *int `yaml:"table,omitempty"` 109 To string `yaml:"to,omitempty"` 110 Type string `yaml:"type,omitempty"` 111 Via string `yaml:"via,omitempty"` 112 Metric *int `yaml:"metric,omitempty"` 113 } 114 115 type RoutePolicy struct { 116 From string `yaml:"from,omitempty"` 117 Mark *int `yaml:"mark,omitempty"` 118 Priority *int `yaml:"priority,omitempty"` 119 Table *int `yaml:"table,omitempty"` 120 To string `yaml:"to,omitempty"` 121 TypeOfService *int `yaml:"type-of-service,omitempty"` 122 } 123 124 type Network struct { 125 Version int `yaml:"version"` 126 Renderer string `yaml:"renderer,omitempty"` 127 Ethernets map[string]Ethernet `yaml:"ethernets,omitempty"` 128 Wifis map[string]Wifi `yaml:"wifis,omitempty"` 129 Bridges map[string]Bridge `yaml:"bridges,omitempty"` 130 Bonds map[string]Bond `yaml:"bonds,omitempty"` 131 VLANs map[string]VLAN `yaml:"vlans,omitempty"` 132 Routes []Route `yaml:"routes,omitempty"` 133 } 134 135 type Netplan struct { 136 Network Network `yaml:"network"` 137 sourceDirectory string 138 sourceFiles []string 139 backedFiles map[string]string 140 writtenFile string 141 } 142 143 // VLAN represents the structures for defining VLAN sections 144 type VLAN struct { 145 Id *int `yaml:"id,omitempty"` 146 Link string `yaml:"link,omitempty"` 147 Interface `yaml:",inline"` 148 } 149 150 // Bond is the interface definition of the bonds: section of netplan 151 type Bond struct { 152 Interfaces []string `yaml:"interfaces,omitempty,flow"` 153 Interface `yaml:",inline"` 154 Parameters BondParameters `yaml:"parameters,omitempty"` 155 } 156 157 // IntString is used to specialize values that can be integers or strings 158 type IntString struct { 159 Int *int 160 String *string 161 } 162 163 func (i *IntString) UnmarshalYAML(unmarshal func(interface{}) error) error { 164 var asInt int 165 var err error 166 if err = unmarshal(&asInt); err == nil { 167 i.Int = &asInt 168 return nil 169 } 170 var asString string 171 if err = unmarshal(&asString); err == nil { 172 i.String = &asString 173 return nil 174 } 175 return errors.Annotatef(err, "not valid as an int or a string") 176 } 177 178 func (i IntString) MarshalYAML() (interface{}, error) { 179 if i.Int != nil { 180 return *i.Int, nil 181 } else if i.String != nil { 182 return *i.String, nil 183 } 184 return nil, nil 185 } 186 187 // For a definition of what netplan supports see here: 188 // https://github.com/CanonicalLtd/netplan/blob/7afef6af053794a400d96f89a81c938c08420783/src/parse.c#L1180 189 // For a definition of what the parameters mean or what values they can contain, see here: 190 // https://www.kernel.org/doc/Documentation/networking/bonding.txt 191 // Note that most parameters can be specified as integers or as strings, which you need to be careful with YAML 192 // as it defaults to strongly typing them. 193 // TODO: (jam 2018-05-14) Should we be sorting the attributes alphabetically? 194 type BondParameters struct { 195 Mode IntString `yaml:"mode,omitempty"` 196 LACPRate IntString `yaml:"lacp-rate,omitempty"` 197 MIIMonitorInterval *int `yaml:"mii-monitor-interval,omitempty"` 198 MinLinks *int `yaml:"min-links,omitempty"` 199 TransmitHashPolicy string `yaml:"transmit-hash-policy,omitempty"` 200 ADSelect IntString `yaml:"ad-select,omitempty"` 201 AllSlavesActive *bool `yaml:"all-slaves-active,omitempty"` 202 ARPInterval *int `yaml:"arp-interval,omitempty"` 203 ARPIPTargets []string `yaml:"arp-ip-targets,omitempty"` 204 ARPValidate IntString `yaml:"arp-validate,omitempty"` 205 ARPAllTargets IntString `yaml:"arp-all-targets,omitempty"` 206 UpDelay *int `yaml:"up-delay,omitempty"` 207 DownDelay *int `yaml:"down-delay,omitempty"` 208 FailOverMACPolicy IntString `yaml:"fail-over-mac-policy,omitempty"` 209 // Netplan misspelled this as 'gratuitious-arp', not sure if it works with that name. 210 // We may need custom handling of both spellings. 211 GratuitousARP *int `yaml:"gratuitious-arp,omitempty"` // nolint: misspell 212 PacketsPerSlave *int `yaml:"packets-per-slave,omitempty"` 213 PrimaryReselectPolicy IntString `yaml:"primary-reselect-policy,omitempty"` 214 ResendIGMP *int `yaml:"resend-igmp,omitempty"` 215 // bonding.txt says that this can be a value from 1-0x7fffffff, should we be forcing it to be a hex value? 216 LearnPacketInterval *int `yaml:"learn-packet-interval,omitempty"` 217 Primary string `yaml:"primary,omitempty"` 218 } 219 220 // BridgeEthernetById takes a deviceId and creates a bridge with this device 221 // using this devices config 222 func (np *Netplan) BridgeEthernetById(deviceId string, bridgeName string) (err error) { 223 ethernet, ok := np.Network.Ethernets[deviceId] 224 if !ok { 225 return errors.NotFoundf("ethernet device with id %q for bridge %q", deviceId, bridgeName) 226 } 227 shouldCreate, err := np.shouldCreateBridge(deviceId, bridgeName) 228 if !shouldCreate { 229 // err may be nil, but we shouldn't continue creating 230 return errors.Trace(err) 231 } 232 np.createBridgeFromInterface(bridgeName, deviceId, ðernet.Interface) 233 np.Network.Ethernets[deviceId] = ethernet 234 return nil 235 } 236 237 // BridgeVLANById takes a deviceId and creates a bridge with this device 238 // using this devices config 239 func (np *Netplan) BridgeVLANById(deviceId string, bridgeName string) (err error) { 240 vlan, ok := np.Network.VLANs[deviceId] 241 if !ok { 242 return errors.NotFoundf("VLAN device with id %q for bridge %q", deviceId, bridgeName) 243 } 244 shouldCreate, err := np.shouldCreateBridge(deviceId, bridgeName) 245 if !shouldCreate { 246 // err may be nil, but we shouldn't continue creating 247 return errors.Trace(err) 248 } 249 np.createBridgeFromInterface(bridgeName, deviceId, &vlan.Interface) 250 np.Network.VLANs[deviceId] = vlan 251 return nil 252 } 253 254 // BridgeBondById takes a deviceId and creates a bridge with this device 255 // using this devices config 256 func (np *Netplan) BridgeBondById(deviceId string, bridgeName string) (err error) { 257 bond, ok := np.Network.Bonds[deviceId] 258 if !ok { 259 return errors.NotFoundf("bond device with id %q for bridge %q", deviceId, bridgeName) 260 } 261 shouldCreate, err := np.shouldCreateBridge(deviceId, bridgeName) 262 if !shouldCreate { 263 // err may be nil, but we shouldn't continue creating 264 return errors.Trace(err) 265 } 266 np.createBridgeFromInterface(bridgeName, deviceId, &bond.Interface) 267 np.Network.Bonds[deviceId] = bond 268 return nil 269 } 270 271 // shouldCreateBridge returns true only if it is clear the bridge doesn't already exist, and that the existing device 272 // isn't in a different bridge. 273 func (np *Netplan) shouldCreateBridge(deviceId string, bridgeName string) (bool, error) { 274 for bName, bridge := range np.Network.Bridges { 275 for _, i := range bridge.Interfaces { 276 if i == deviceId { 277 // The device is already properly bridged, nothing to do 278 if bridgeName == bName { 279 return false, nil 280 } else { 281 return false, errors.AlreadyExistsf("cannot create bridge %q, device %q in bridge %q", bridgeName, deviceId, bName) 282 } 283 } 284 } 285 if bridgeName == bName { 286 return false, errors.AlreadyExistsf( 287 "cannot create bridge %q with device %q - bridge %q w/ interfaces %q", 288 bridgeName, deviceId, bridgeName, strings.Join(bridge.Interfaces, ", ")) 289 } 290 } 291 return true, nil 292 } 293 294 // createBridgeFromInterface will create a bridge stealing the interface details, and wiping the existing interface 295 // except for MTU so that IP Address information is never duplicated. 296 func (np *Netplan) createBridgeFromInterface(bridgeName, deviceId string, intf *Interface) { 297 if np.Network.Bridges == nil { 298 np.Network.Bridges = make(map[string]Bridge) 299 } 300 np.Network.Bridges[bridgeName] = Bridge{ 301 Interfaces: []string{deviceId}, 302 Interface: *intf, 303 } 304 *intf = Interface{MTU: intf.MTU} 305 } 306 307 // Marshal a Netplan instance into YAML. 308 func Marshal(in *Netplan) (out []byte, err error) { 309 return goyaml.Marshal(in) 310 } 311 312 type sortableDirEntries []os.DirEntry 313 314 func (fil sortableDirEntries) Len() int { 315 return len(fil) 316 } 317 318 func (fil sortableDirEntries) Less(i, j int) bool { 319 return fil[i].Name() < fil[j].Name() 320 } 321 322 func (fil sortableDirEntries) Swap(i, j int) { 323 fil[i], fil[j] = fil[j], fil[i] 324 } 325 326 // ReadDirectory reads the contents of a netplan directory and 327 // returns complete config. 328 func ReadDirectory(dirPath string) (np Netplan, err error) { 329 dirEntries, err := os.ReadDir(dirPath) 330 if err != nil { 331 return np, err 332 } 333 np.sourceDirectory = dirPath 334 sortedDirEntries := sortableDirEntries(dirEntries) 335 sort.Sort(sortedDirEntries) 336 337 // First, unmarshal all configuration files into maps and merge them. 338 // Since the file list is pre-sorted, the first unmarshalled file 339 // serves as the base configuration; subsequent configuration maps are 340 // merged into it. 341 var mergedConfig map[interface{}]interface{} 342 for _, dirEntry := range sortedDirEntries { 343 if !dirEntry.IsDir() && strings.HasSuffix(dirEntry.Name(), ".yaml") { 344 np.sourceFiles = append(np.sourceFiles, dirEntry.Name()) 345 346 pathToConfig := path.Join(np.sourceDirectory, dirEntry.Name()) 347 configContents, err := os.ReadFile(pathToConfig) 348 if err != nil { 349 return Netplan{}, errors.Annotatef(err, "reading netplan configuration from %q", pathToConfig) 350 } 351 352 var unmarshaledContents map[interface{}]interface{} 353 if err := goyaml.Unmarshal(configContents, &unmarshaledContents); err != nil { 354 return Netplan{}, errors.Annotatef(err, "unmarshaling netplan configuration from %q", pathToConfig) 355 356 } 357 358 if mergedConfig == nil { 359 mergedConfig = unmarshaledContents 360 } else { 361 mergedResult, err := mergeNetplanConfigs(mergedConfig, unmarshaledContents) 362 if err != nil { 363 return Netplan{}, errors.Annotatef(err, "merging netplan configuration from %s", pathToConfig) 364 } 365 366 // mergeNetplanConfigs should return back the 367 // value we passed in. However, lets be extra 368 // paranoid and double check that a malicious 369 // file did not mutate the type of the returned 370 // value. 371 mergedConfigMap, ok := mergedResult.(map[interface{}]interface{}) 372 if !ok { 373 return Netplan{}, errors.Errorf("merging netplan configuration from %s caused the original configuration to become corrupted", pathToConfig) 374 } 375 mergedConfig = mergedConfigMap 376 } 377 } 378 } 379 380 // Serialize the merged config back into yaml and unmarshal it using 381 // strict mode it to ensure that the presence of any unknown field 382 // triggers an error. 383 // 384 // As juju mutates the unmashaled Netplan struct and writes it back to 385 // disk, using strict mode guarantees that we will never accidentally 386 // drop netplan config values just because they were not defined in 387 // our structs. 388 mergedYAML, err := goyaml.Marshal(mergedConfig) 389 if err != nil { 390 return Netplan{}, errors.Trace(err) 391 } else if err := goyaml.UnmarshalStrict(mergedYAML, &np); err != nil { 392 return Netplan{}, errors.Trace(err) 393 } 394 return np, nil 395 } 396 397 // mergeNetplanConfigs recursively merges two netplan configurations where 398 // values is src will overwrite values in dst based on the rules described in 399 // http://manpages.ubuntu.com/manpages/groovy/man8/netplan-generate.8.html: 400 // 401 // - If the values are YAML boolean or scalar values (numbers and strings) 402 // the old value is overwritten by the new value. 403 // 404 // - If the values are sequences, the sequences are concatenated - the new 405 // values are appended to the old list. 406 // 407 // - If the values are mappings, netplan will examine the elements of the 408 // mappings in turn using these rules. 409 // 410 // The function returns back the merged destination object which *may* be 411 // different than the one that was passed into the function (e.g. if dst was 412 // a map or slice that got resized). 413 func mergeNetplanConfigs(dst, src interface{}) (interface{}, error) { 414 if dst == nil { 415 return src, nil 416 } 417 418 var err error 419 switch dstVal := dst.(type) { 420 case map[interface{}]interface{}: 421 srcVal, ok := src.(map[interface{}]interface{}) 422 if !ok { 423 return nil, errors.Errorf("configuration values have different types (destination: %T, src: %T)", dst, src) 424 } 425 426 // Overwrite values in dst for keys that are present in both 427 // dst and src. 428 for dstMapKey, dstMapVal := range dstVal { 429 srcMapVal, exists := srcVal[dstMapKey] 430 if !exists { 431 continue 432 } 433 434 // Merge recursively (if non-scalar values) 435 dstVal[dstMapKey], err = mergeNetplanConfigs(dstMapVal, srcMapVal) 436 if err != nil { 437 return nil, errors.Annotatef(err, "merging configuration key %q", dstMapKey) 438 } 439 } 440 441 // Now append values from src for keys that are not present in 442 // the dst map. However, if the dstVal is nil, just use the 443 // srcVal as-is. 444 if dstVal == nil { 445 return srcVal, nil 446 } 447 448 for srcMapKey, srcMapVal := range srcVal { 449 _, exists := dstVal[srcMapKey] 450 if exists { 451 continue 452 } 453 454 // Insert new value into the destination. 455 dstVal[srcMapKey] = srcMapVal 456 } 457 458 return dstVal, nil 459 case []interface{}: 460 srcVal, ok := src.([]interface{}) 461 if !ok { 462 return nil, errors.Errorf("configuration values have different types (destination: %T, src: %T)", dst, src) 463 } 464 465 // Only append missing values to the slice 466 dstLen := len(dstVal) 467 nextSrcSliceVal: 468 for _, srcSliceVal := range srcVal { 469 // If the srcSliceVal is not present in any of the 470 // original dstVal entries then append it. Note that we 471 // don't care about the values that may get potentially 472 // appended, hence the pre-calculation of the dstVal 473 // length. 474 for i := 0; i < dstLen; i++ { 475 if reflect.DeepEqual(dstVal[i], srcSliceVal) { 476 continue nextSrcSliceVal // value already present 477 } 478 } 479 480 dstVal = append(dstVal, srcSliceVal) 481 } 482 return dstVal, nil 483 default: 484 // Assume a scalar value and overwrite with src 485 return src, nil 486 } 487 } 488 489 // MoveYamlsToBak moves source .yaml files in a directory to .yaml.bak.(timestamp), except 490 func (np *Netplan) MoveYamlsToBak() (err error) { 491 if np.backedFiles != nil { 492 return errors.Errorf("Cannot backup netplan yamls twice") 493 } 494 suffix := fmt.Sprintf(".bak.%d", time.Now().Unix()) 495 np.backedFiles = make(map[string]string) 496 for _, file := range np.sourceFiles { 497 newFilename := fmt.Sprintf("%s%s", file, suffix) 498 oldFile := path.Join(np.sourceDirectory, file) 499 newFile := path.Join(np.sourceDirectory, newFilename) 500 err = os.Rename(oldFile, newFile) 501 if err != nil { 502 logger.Errorf("Cannot rename %s to %s - %q", oldFile, newFile, err.Error()) 503 } 504 np.backedFiles[oldFile] = newFile 505 } 506 return nil 507 } 508 509 // Write writes merged netplan yaml to file specified by path. If path is empty filename is autogenerated 510 func (np *Netplan) Write(inPath string) (filePath string, err error) { 511 if np.writtenFile != "" { 512 return "", errors.Errorf("Cannot write the same netplan twice") 513 } 514 if inPath == "" { 515 i := 99 516 for ; i > 0; i-- { 517 filePath = path.Join(np.sourceDirectory, fmt.Sprintf("%0.2d-juju.yaml", i)) 518 _, err = os.Stat(filePath) 519 if os.IsNotExist(err) { 520 break 521 } 522 } 523 if i == 0 { 524 return "", errors.Errorf("Can't generate a filename for netplan YAML") 525 } 526 } else { 527 filePath = inPath 528 } 529 tmpFilePath := fmt.Sprintf("%s.tmp.%d", filePath, time.Now().UnixNano()) 530 out, err := Marshal(np) 531 if err != nil { 532 return "", err 533 } 534 err = os.WriteFile(tmpFilePath, out, 0644) 535 if err != nil { 536 return "", err 537 } 538 err = os.Rename(tmpFilePath, filePath) 539 if err != nil { 540 return "", err 541 } 542 np.writtenFile = filePath 543 return filePath, nil 544 } 545 546 // Rollback moves backed up files to original locations and removes written file 547 func (np *Netplan) Rollback() (err error) { 548 if np.writtenFile != "" { 549 os.Remove(np.writtenFile) 550 } 551 for oldFile, newFile := range np.backedFiles { 552 err = os.Rename(newFile, oldFile) 553 if err != nil { 554 logger.Errorf("Cannot rename %s to %s - %q", newFile, oldFile, err.Error()) 555 } 556 } 557 np.backedFiles = nil 558 np.writtenFile = "" 559 return nil 560 } 561 562 func (np *Netplan) FindEthernetByMAC(mac string) (device string, err error) { 563 for id, ethernet := range np.Network.Ethernets { 564 if v, ok := ethernet.Match["macaddress"]; ok && v == mac { 565 return id, nil 566 } 567 if ethernet.MACAddress == mac { 568 return id, nil 569 } 570 } 571 return "", errors.NotFoundf("Ethernet device with MAC %q", mac) 572 } 573 574 func (np *Netplan) FindEthernetByName(name string) (device string, err error) { 575 for id, ethernet := range np.Network.Ethernets { 576 if matchName, ok := ethernet.Match["name"]; ok { 577 // Netplan uses simple wildcards for name matching - so does filepath.Match 578 if match, err := filepath.Match(matchName, name); err == nil && match { 579 return id, nil 580 } 581 } 582 if ethernet.SetName == name { 583 return id, nil 584 } 585 } 586 if _, ok := np.Network.Ethernets[name]; ok { 587 return name, nil 588 } 589 return "", errors.NotFoundf("Ethernet device with name %q", name) 590 } 591 592 func (np *Netplan) FindVLANByMAC(mac string) (device string, err error) { 593 for id, vlan := range np.Network.VLANs { 594 if vlan.MACAddress == mac { 595 return id, nil 596 } 597 } 598 return "", errors.NotFoundf("VLAN device with MAC %q", mac) 599 } 600 601 func (np *Netplan) FindVLANByName(name string) (device string, err error) { 602 if _, ok := np.Network.VLANs[name]; ok { 603 return name, nil 604 } 605 return "", errors.NotFoundf("VLAN device with name %q", name) 606 } 607 608 func (np *Netplan) FindBondByMAC(mac string) (device string, err error) { 609 for id, bonds := range np.Network.Bonds { 610 if bonds.MACAddress == mac { 611 return id, nil 612 } 613 } 614 return "", errors.NotFoundf("bond device with MAC %q", mac) 615 } 616 617 func (np *Netplan) FindBondByName(name string) (device string, err error) { 618 if _, ok := np.Network.Bonds[name]; ok { 619 return name, nil 620 } 621 return "", errors.NotFoundf("bond device with name %q", name) 622 } 623 624 type DeviceType string 625 626 const ( 627 TypeEthernet = DeviceType("ethernet") 628 TypeVLAN = DeviceType("vlan") 629 TypeBond = DeviceType("bond") 630 ) 631 632 // FindDeviceByMACOrName will look for an Ethernet, VLAN or Bond matching the Name of the device or its MAC address. 633 // Name is preferred to MAC address. 634 func (np *Netplan) FindDeviceByNameOrMAC(name, mac string) (string, DeviceType, error) { 635 if name != "" { 636 bond, err := np.FindBondByName(name) 637 if err == nil { 638 return bond, TypeBond, nil 639 } 640 if !errors.IsNotFound(err) { 641 return "", "", errors.Trace(err) 642 } 643 vlan, err := np.FindVLANByName(name) 644 if err == nil { 645 return vlan, TypeVLAN, nil 646 } 647 ethernet, err := np.FindEthernetByName(name) 648 if err == nil { 649 return ethernet, TypeEthernet, nil 650 } 651 652 } 653 // by MAC is less reliable because things like vlans often have the same MAC address 654 if mac != "" { 655 bond, err := np.FindBondByMAC(mac) 656 if err == nil { 657 return bond, TypeBond, nil 658 } 659 if !errors.IsNotFound(err) { 660 return "", "", errors.Trace(err) 661 } 662 vlan, err := np.FindVLANByMAC(mac) 663 if err == nil { 664 return vlan, TypeVLAN, nil 665 } 666 ethernet, err := np.FindEthernetByMAC(mac) 667 if err == nil { 668 return ethernet, TypeEthernet, nil 669 } 670 } 671 return "", "", errors.NotFoundf("device - name %q MAC %q", name, mac) 672 }