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, &ethernet.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  }