github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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 "io/ioutil" 9 "os" 10 "path" 11 "path/filepath" 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 49 // Ethernet defines fields for just Ethernet devices 50 type Ethernet struct { 51 Match map[string]string `yaml:"match,omitempty"` 52 Wakeonlan bool `yaml:"wakeonlan,omitempty"` 53 SetName string `yaml:"set-name,omitempty"` 54 Interface `yaml:",inline"` 55 } 56 type AccessPoint struct { 57 Password string `yaml:"password,omitempty"` 58 Mode string `yaml:"mode,omitempty"` 59 Channel int `yaml:"channel,omitempty"` 60 } 61 type Wifi struct { 62 Match map[string]string `yaml:"match,omitempty"` 63 SetName string `yaml:"set-name,omitempty"` 64 Wakeonlan bool `yaml:"wakeonlan,omitempty"` 65 AccessPoints map[string]AccessPoint `yaml:"access-points,omitempty"` 66 Interface `yaml:",inline"` 67 } 68 69 type BridgeParameters struct { 70 AgeingTime *int `yaml:"ageing-time,omitempty"` 71 ForwardDelay *int `yaml:"forward-delay,omitempty"` 72 HelloTime *int `yaml:"hello-time,omitempty"` 73 MaxAge *int `yaml:"max-age,omitempty"` 74 PathCost map[string]int `yaml:"path-cost,omitempty"` 75 PortPriority map[string]int `yaml:"port-priority,omitempty"` 76 Priority *int `yaml:"priority,omitempty"` 77 STP *bool `yaml:"stp,omitempty"` 78 } 79 80 type Bridge struct { 81 Interfaces []string `yaml:"interfaces,omitempty,flow"` 82 Interface `yaml:",inline"` 83 Parameters BridgeParameters `yaml:"parameters,omitempty"` 84 } 85 86 type Route struct { 87 From string `yaml:"from,omitempty"` 88 OnLink *bool `yaml:"on-link,omitempty"` 89 Scope string `yaml:"scope,omitempty"` 90 Table *int `yaml:"table,omitempty"` 91 To string `yaml:"to,omitempty"` 92 Type string `yaml:"type,omitempty"` 93 Via string `yaml:"via,omitempty"` 94 Metric *int `yaml:"metric,omitempty"` 95 } 96 97 type RoutePolicy struct { 98 From string `yaml:"from,omitempty"` 99 Mark *int `yaml:"mark,omitempty"` 100 Priority *int `yaml:"priority,omitempty"` 101 Table *int `yaml:"table,omitempty"` 102 To string `yaml:"to,omitempty"` 103 TypeOfService *int `yaml:"type-of-service,omitempty"` 104 } 105 106 type Network struct { 107 Version int `yaml:"version"` 108 Renderer string `yaml:"renderer,omitempty"` 109 Ethernets map[string]Ethernet `yaml:"ethernets,omitempty"` 110 Wifis map[string]Wifi `yaml:"wifis,omitempty"` 111 Bridges map[string]Bridge `yaml:"bridges,omitempty"` 112 Bonds map[string]Bond `yaml:"bonds,omitempty"` 113 VLANs map[string]VLAN `yaml:"vlans,omitempty"` 114 Routes []Route `yaml:"routes,omitempty"` 115 } 116 117 type Netplan struct { 118 Network Network `yaml:"network"` 119 sourceDirectory string 120 sourceFiles []string 121 backedFiles map[string]string 122 writtenFile string 123 } 124 125 // VLAN represents the structures for defining VLAN sections 126 type VLAN struct { 127 Id *int `yaml:"id,omitempty"` 128 Link string `yaml:"link,omitempty"` 129 Interface `yaml:",inline"` 130 } 131 132 // Bond is the interface definition of the bonds: section of netplan 133 type Bond struct { 134 Interfaces []string `yaml:"interfaces,omitempty,flow"` 135 Interface `yaml:",inline"` 136 Parameters BondParameters `yaml:"parameters,omitempty"` 137 } 138 139 // IntString is used to specialize values that can be integers or strings 140 type IntString struct { 141 Int *int 142 String *string 143 } 144 145 func (i *IntString) UnmarshalYAML(unmarshal func(interface{}) error) error { 146 var asInt int 147 var err error 148 if err = unmarshal(&asInt); err == nil { 149 i.Int = &asInt 150 return nil 151 } 152 var asString string 153 if err = unmarshal(&asString); err == nil { 154 i.String = &asString 155 return nil 156 } 157 return errors.Annotatef(err, "not valid as an int or a string") 158 } 159 160 func (i IntString) MarshalYAML() (interface{}, error) { 161 if i.Int != nil { 162 return *i.Int, nil 163 } else if i.String != nil { 164 return *i.String, nil 165 } 166 return nil, nil 167 } 168 169 // For a definition of what netplan supports see here: 170 // https://github.com/CanonicalLtd/netplan/blob/7afef6af053794a400d96f89a81c938c08420783/src/parse.c#L1180 171 // For a definition of what the parameters mean or what values they can contain, see here: 172 // https://www.kernel.org/doc/Documentation/networking/bonding.txt 173 // Note that most parameters can be specified as integers or as strings, which you need to be careful with YAML 174 // as it defaults to strongly typing them. 175 // TODO: (jam 2018-05-14) Should we be sorting the attributes alphabetically? 176 type BondParameters struct { 177 Mode IntString `yaml:"mode,omitempty"` 178 LACPRate IntString `yaml:"lacp-rate,omitempty"` 179 MIIMonitorInterval *int `yaml:"mii-monitor-interval,omitempty"` 180 MinLinks *int `yaml:"min-links,omitempty"` 181 TransmitHashPolicy string `yaml:"transmit-hash-policy,omitempty"` 182 ADSelect IntString `yaml:"ad-select,omitempty"` 183 AllSlavesActive *bool `yaml:"all-slaves-active,omitempty"` 184 ARPInterval *int `yaml:"arp-interval,omitempty"` 185 ARPIPTargets []string `yaml:"arp-ip-targets,omitempty"` 186 ARPValidate IntString `yaml:"arp-validate,omitempty"` 187 ARPAllTargets IntString `yaml:"arp-all-targets,omitempty"` 188 UpDelay *int `yaml:"up-delay,omitempty"` 189 DownDelay *int `yaml:"down-delay,omitempty"` 190 FailOverMACPolicy IntString `yaml:"fail-over-mac-policy,omitempty"` 191 // Netplan misspelled this as 'gratuitious-arp', not sure if it works with that name. 192 // We may need custom handling of both spellings. 193 GratuitousARP *int `yaml:"gratuitious-arp,omitempty"` // nolint: misspell 194 PacketsPerSlave *int `yaml:"packets-per-slave,omitempty"` 195 PrimaryReselectPolicy IntString `yaml:"primary-reselect-policy,omitempty"` 196 ResendIGMP *int `yaml:"resend-igmp,omitempty"` 197 // bonding.txt says that this can be a value from 1-0x7fffffff, should we be forcing it to be a hex value? 198 LearnPacketInterval *int `yaml:"learn-packet-interval,omitempty"` 199 Primary string `yaml:"primary,omitempty"` 200 } 201 202 // BridgeEthernetById takes a deviceId and creates a bridge with this device 203 // using this devices config 204 func (np *Netplan) BridgeEthernetById(deviceId string, bridgeName string) (err error) { 205 ethernet, ok := np.Network.Ethernets[deviceId] 206 if !ok { 207 return errors.NotFoundf("ethernet device with id %q for bridge %q", deviceId, bridgeName) 208 } 209 shouldCreate, err := np.shouldCreateBridge(deviceId, bridgeName) 210 if !shouldCreate { 211 // err may be nil, but we shouldn't continue creating 212 return errors.Trace(err) 213 } 214 np.createBridgeFromInterface(bridgeName, deviceId, ðernet.Interface) 215 np.Network.Ethernets[deviceId] = ethernet 216 return nil 217 } 218 219 // BridgeVLANById takes a deviceId and creates a bridge with this device 220 // using this devices config 221 func (np *Netplan) BridgeVLANById(deviceId string, bridgeName string) (err error) { 222 vlan, ok := np.Network.VLANs[deviceId] 223 if !ok { 224 return errors.NotFoundf("VLAN device with id %q for bridge %q", deviceId, bridgeName) 225 } 226 shouldCreate, err := np.shouldCreateBridge(deviceId, bridgeName) 227 if !shouldCreate { 228 // err may be nil, but we shouldn't continue creating 229 return errors.Trace(err) 230 } 231 np.createBridgeFromInterface(bridgeName, deviceId, &vlan.Interface) 232 np.Network.VLANs[deviceId] = vlan 233 return nil 234 } 235 236 // BridgeBondById takes a deviceId and creates a bridge with this device 237 // using this devices config 238 func (np *Netplan) BridgeBondById(deviceId string, bridgeName string) (err error) { 239 bond, ok := np.Network.Bonds[deviceId] 240 if !ok { 241 return errors.NotFoundf("bond device with id %q for bridge %q", deviceId, bridgeName) 242 } 243 shouldCreate, err := np.shouldCreateBridge(deviceId, bridgeName) 244 if !shouldCreate { 245 // err may be nil, but we shouldn't continue creating 246 return errors.Trace(err) 247 } 248 np.createBridgeFromInterface(bridgeName, deviceId, &bond.Interface) 249 np.Network.Bonds[deviceId] = bond 250 return nil 251 } 252 253 // shouldCreateBridge returns true only if it is clear the bridge doesn't already exist, and that the existing device 254 // isn't in a different bridge. 255 func (np *Netplan) shouldCreateBridge(deviceId string, bridgeName string) (bool, error) { 256 for bName, bridge := range np.Network.Bridges { 257 for _, i := range bridge.Interfaces { 258 if i == deviceId { 259 // The device is already properly bridged, nothing to do 260 if bridgeName == bName { 261 return false, nil 262 } else { 263 return false, errors.AlreadyExistsf("cannot create bridge %q, device %q in bridge %q", bridgeName, deviceId, bName) 264 } 265 } 266 } 267 if bridgeName == bName { 268 return false, errors.AlreadyExistsf( 269 "cannot create bridge %q with device %q - bridge %q w/ interfaces %q", 270 bridgeName, deviceId, bridgeName, strings.Join(bridge.Interfaces, ", ")) 271 } 272 } 273 return true, nil 274 } 275 276 // createBridgeFromInterface will create a bridge stealing the interface details, and wiping the existing interface 277 // except for MTU so that IP Address information is never duplicated. 278 func (np *Netplan) createBridgeFromInterface(bridgeName, deviceId string, intf *Interface) { 279 if np.Network.Bridges == nil { 280 np.Network.Bridges = make(map[string]Bridge) 281 } 282 np.Network.Bridges[bridgeName] = Bridge{ 283 Interfaces: []string{deviceId}, 284 Interface: *intf, 285 } 286 *intf = Interface{MTU: intf.MTU} 287 } 288 289 func (np *Netplan) merge(other *Netplan) { 290 // Only copy attributes that would be unmarshalled from yaml. 291 // This blithely replaces keys in the maps (eg. Ethernets or 292 // Wifis) if they're set in both np and other - it's not clear 293 // from the reference whether this is the right thing to do. 294 // See https://bugs.launchpad.net/juju/+bug/1701429 and 295 // https://netplan.io/reference#general-structure 296 np.Network.Version = other.Network.Version 297 np.Network.Renderer = other.Network.Renderer 298 np.Network.Routes = other.Network.Routes 299 if np.Network.Ethernets == nil { 300 np.Network.Ethernets = other.Network.Ethernets 301 } else { 302 for key, val := range other.Network.Ethernets { 303 np.Network.Ethernets[key] = val 304 } 305 } 306 if np.Network.Wifis == nil { 307 np.Network.Wifis = other.Network.Wifis 308 } else { 309 for key, val := range other.Network.Wifis { 310 np.Network.Wifis[key] = val 311 } 312 } 313 if np.Network.Bridges == nil { 314 np.Network.Bridges = other.Network.Bridges 315 } else { 316 for key, val := range other.Network.Bridges { 317 np.Network.Bridges[key] = val 318 } 319 } 320 if np.Network.Bonds == nil { 321 np.Network.Bonds = other.Network.Bonds 322 } else { 323 for key, val := range other.Network.Bonds { 324 np.Network.Bonds[key] = val 325 } 326 } 327 if np.Network.VLANs == nil { 328 np.Network.VLANs = other.Network.VLANs 329 } else { 330 for key, val := range other.Network.VLANs { 331 np.Network.VLANs[key] = val 332 } 333 } 334 } 335 336 func Unmarshal(in []byte, out *Netplan) error { 337 if out == nil { 338 return errors.NotValidf("nil out Netplan") 339 } 340 // Use UnmarshalStrict because we want errors for unknown 341 // attributes. This also refuses to overwrite keys (which we need) 342 // so unmarshal locally and copy across. 343 var local Netplan 344 if err := goyaml.UnmarshalStrict(in, &local); err != nil { 345 return errors.Trace(err) 346 } 347 out.merge(&local) 348 return nil 349 } 350 351 func Marshal(in *Netplan) (out []byte, err error) { 352 return goyaml.Marshal(in) 353 } 354 355 // readYamlFile reads netplan yaml into existing netplan structure 356 // TODO(wpk) 2017-06-14 When reading files sequentially netplan replaces single 357 // keys with new values, we have to simulate this behaviour. 358 // https://bugs.launchpad.net/juju/+bug/1701429 359 func (np *Netplan) readYamlFile(path string) (err error) { 360 contents, err := ioutil.ReadFile(path) 361 if err != nil { 362 return err 363 } 364 err = Unmarshal(contents, np) 365 if err != nil { 366 return err 367 } 368 369 return nil 370 } 371 372 type sortableFileInfos []os.FileInfo 373 374 func (fil sortableFileInfos) Len() int { 375 return len(fil) 376 } 377 378 func (fil sortableFileInfos) Less(i, j int) bool { 379 return fil[i].Name() < fil[j].Name() 380 } 381 382 func (fil sortableFileInfos) Swap(i, j int) { 383 fil[i], fil[j] = fil[j], fil[i] 384 } 385 386 // ReadDirectory reads the contents of a netplan directory and 387 // returns complete config. 388 func ReadDirectory(dirPath string) (np Netplan, err error) { 389 fileInfos, err := ioutil.ReadDir(dirPath) 390 if err != nil { 391 return np, err 392 } 393 np.sourceDirectory = dirPath 394 sortedFileInfos := sortableFileInfos(fileInfos) 395 sort.Sort(sortedFileInfos) 396 for _, fileInfo := range sortedFileInfos { 397 if !fileInfo.IsDir() && strings.HasSuffix(fileInfo.Name(), ".yaml") { 398 np.sourceFiles = append(np.sourceFiles, fileInfo.Name()) 399 } 400 } 401 for _, fileName := range np.sourceFiles { 402 err := np.readYamlFile(path.Join(np.sourceDirectory, fileName)) 403 if err != nil { 404 return np, err 405 } 406 } 407 return np, nil 408 } 409 410 // MoveYamlsToBak moves source .yaml files in a directory to .yaml.bak.(timestamp), except 411 func (np *Netplan) MoveYamlsToBak() (err error) { 412 if np.backedFiles != nil { 413 return errors.Errorf("Cannot backup netplan yamls twice") 414 } 415 suffix := fmt.Sprintf(".bak.%d", time.Now().Unix()) 416 np.backedFiles = make(map[string]string) 417 for _, file := range np.sourceFiles { 418 newFilename := fmt.Sprintf("%s%s", file, suffix) 419 oldFile := path.Join(np.sourceDirectory, file) 420 newFile := path.Join(np.sourceDirectory, newFilename) 421 err = os.Rename(oldFile, newFile) 422 if err != nil { 423 logger.Errorf("Cannot rename %s to %s - %q", oldFile, newFile, err.Error()) 424 } 425 np.backedFiles[oldFile] = newFile 426 } 427 return nil 428 } 429 430 // Write writes merged netplan yaml to file specified by path. If path is empty filename is autogenerated 431 func (np *Netplan) Write(inPath string) (filePath string, err error) { 432 if np.writtenFile != "" { 433 return "", errors.Errorf("Cannot write the same netplan twice") 434 } 435 if inPath == "" { 436 i := 99 437 for ; i > 0; i-- { 438 filePath = path.Join(np.sourceDirectory, fmt.Sprintf("%0.2d-juju.yaml", i)) 439 _, err = os.Stat(filePath) 440 if os.IsNotExist(err) { 441 break 442 } 443 } 444 if i == 0 { 445 return "", errors.Errorf("Can't generate a filename for netplan YAML") 446 } 447 } else { 448 filePath = inPath 449 } 450 tmpFilePath := fmt.Sprintf("%s.tmp.%d", filePath, time.Now().UnixNano()) 451 out, err := Marshal(np) 452 if err != nil { 453 return "", err 454 } 455 err = ioutil.WriteFile(tmpFilePath, out, 0644) 456 if err != nil { 457 return "", err 458 } 459 err = os.Rename(tmpFilePath, filePath) 460 if err != nil { 461 return "", err 462 } 463 np.writtenFile = filePath 464 return filePath, nil 465 } 466 467 // Rollback moves backed up files to original locations and removes written file 468 func (np *Netplan) Rollback() (err error) { 469 if np.writtenFile != "" { 470 os.Remove(np.writtenFile) 471 } 472 for oldFile, newFile := range np.backedFiles { 473 err = os.Rename(newFile, oldFile) 474 if err != nil { 475 logger.Errorf("Cannot rename %s to %s - %q", newFile, oldFile, err.Error()) 476 } 477 } 478 np.backedFiles = nil 479 np.writtenFile = "" 480 return nil 481 } 482 483 func (np *Netplan) FindEthernetByMAC(mac string) (device string, err error) { 484 for id, ethernet := range np.Network.Ethernets { 485 if v, ok := ethernet.Match["macaddress"]; ok && v == mac { 486 return id, nil 487 } 488 if ethernet.MACAddress == mac { 489 return id, nil 490 } 491 } 492 return "", errors.NotFoundf("Ethernet device with MAC %q", mac) 493 } 494 495 func (np *Netplan) FindEthernetByName(name string) (device string, err error) { 496 for id, ethernet := range np.Network.Ethernets { 497 if matchName, ok := ethernet.Match["name"]; ok { 498 // Netplan uses simple wildcards for name matching - so does filepath.Match 499 if match, err := filepath.Match(matchName, name); err == nil && match { 500 return id, nil 501 } 502 } 503 if ethernet.SetName == name { 504 return id, nil 505 } 506 } 507 if _, ok := np.Network.Ethernets[name]; ok { 508 return name, nil 509 } 510 return "", errors.NotFoundf("Ethernet device with name %q", name) 511 } 512 513 func (np *Netplan) FindVLANByMAC(mac string) (device string, err error) { 514 for id, vlan := range np.Network.VLANs { 515 if vlan.MACAddress == mac { 516 return id, nil 517 } 518 } 519 return "", errors.NotFoundf("VLAN device with MAC %q", mac) 520 } 521 522 func (np *Netplan) FindVLANByName(name string) (device string, err error) { 523 if _, ok := np.Network.VLANs[name]; ok { 524 return name, nil 525 } 526 return "", errors.NotFoundf("VLAN device with name %q", name) 527 } 528 529 func (np *Netplan) FindBondByMAC(mac string) (device string, err error) { 530 for id, bonds := range np.Network.Bonds { 531 if bonds.MACAddress == mac { 532 return id, nil 533 } 534 } 535 return "", errors.NotFoundf("bond device with MAC %q", mac) 536 } 537 538 func (np *Netplan) FindBondByName(name string) (device string, err error) { 539 if _, ok := np.Network.Bonds[name]; ok { 540 return name, nil 541 } 542 return "", errors.NotFoundf("bond device with name %q", name) 543 } 544 545 type DeviceType string 546 547 const ( 548 TypeEthernet = DeviceType("ethernet") 549 TypeVLAN = DeviceType("vlan") 550 TypeBond = DeviceType("bond") 551 ) 552 553 // FindDeviceByMACOrName will look for an Ethernet, VLAN or Bond matching the Name of the device or its MAC address. 554 // Name is preferred to MAC address. 555 func (np *Netplan) FindDeviceByNameOrMAC(name, mac string) (string, DeviceType, error) { 556 if name != "" { 557 bond, err := np.FindBondByName(name) 558 if err == nil { 559 return bond, TypeBond, nil 560 } 561 if !errors.IsNotFound(err) { 562 return "", "", errors.Trace(err) 563 } 564 vlan, err := np.FindVLANByName(name) 565 if err == nil { 566 return vlan, TypeVLAN, nil 567 } 568 ethernet, err := np.FindEthernetByName(name) 569 if err == nil { 570 return ethernet, TypeEthernet, nil 571 } 572 573 } 574 // by MAC is less reliable because things like vlans often have the same MAC address 575 if mac != "" { 576 bond, err := np.FindBondByMAC(mac) 577 if err == nil { 578 return bond, TypeBond, nil 579 } 580 if !errors.IsNotFound(err) { 581 return "", "", errors.Trace(err) 582 } 583 vlan, err := np.FindVLANByMAC(mac) 584 if err == nil { 585 return vlan, TypeVLAN, nil 586 } 587 ethernet, err := np.FindEthernetByMAC(mac) 588 if err == nil { 589 return ethernet, TypeEthernet, nil 590 } 591 } 592 return "", "", errors.NotFoundf("device - name %q MAC %q", name, mac) 593 }