diff --git a/cloudconfig/containerinit/container_userdata.go b/cloudconfig/containerinit/container_userdata.go index bb6c7cb69b7..2142dad2e79 100644 --- a/cloudconfig/containerinit/container_userdata.go +++ b/cloudconfig/containerinit/container_userdata.go @@ -188,7 +188,8 @@ func GenerateNetplan(networkConfig *container.NetworkConfig) (string, error) { if cidr := info.CIDRAddress(); cidr != "" { iface.Addresses = append(iface.Addresses, cidr) } else if info.ConfigType == network.ConfigDHCP { - iface.Dhcp4 = true + t := true + iface.DHCP4 = &t } for _, dns := range info.DNSServers { @@ -217,7 +218,7 @@ func GenerateNetplan(networkConfig *container.NetworkConfig) (string, error) { route := netplan.Route{ To: route.DestinationCIDR, Via: route.GatewayIP, - Metric: route.Metric, + Metric: &route.Metric, } iface.Routes = append(iface.Routes, route) } diff --git a/network/netplan/activate.go b/network/netplan/activate.go index a895205d82f..cb452c39e1c 100644 --- a/network/netplan/activate.go +++ b/network/netplan/activate.go @@ -26,8 +26,8 @@ type ActivationParams struct { // ActivationResult captures the result of actively bridging the // interfaces using ifup/ifdown. type ActivationResult struct { - Stdout []byte - Stderr []byte + Stdout string + Stderr string Code int } @@ -48,19 +48,28 @@ func BridgeAndActivate(params ActivationParams) (*ActivationResult, error) { for _, device := range params.Devices { var deviceId string - err := errors.NotFoundf("No such device - name %q MAC %q", device.DeviceName, device.MACAddress) - if device.MACAddress != "" { - deviceId, err = netplan.FindEthernetByMAC(device.MACAddress) - } - if err != nil && device.DeviceName != "" { - deviceId, err = netplan.FindEthernetByName(device.DeviceName) - } + deviceId, deviceType, err := netplan.FindDeviceByNameOrMAC(device.DeviceName, device.MACAddress) if err != nil { - return nil, err + return nil, errors.Trace(err) } - err = netplan.BridgeEthernetById(deviceId, device.BridgeName) - if err != nil { - return nil, err + switch deviceType { + case TypeEthernet: + err = netplan.BridgeEthernetById(deviceId, device.BridgeName) + if err != nil { + return nil, err + } + case TypeBond: + err = netplan.BridgeBondById(deviceId, device.BridgeName) + if err != nil { + return nil, err + } + case TypeVLAN: + err = netplan.BridgeVLANById(deviceId, device.BridgeName) + if err != nil { + return nil, err + } + default: + return nil, errors.Errorf("unable to create bridge for %q, unknown device type %q", deviceId, deviceType) } } _, err = netplan.Write("") @@ -82,8 +91,8 @@ func BridgeAndActivate(params ActivationParams) (*ActivationResult, error) { result, err := scriptrunner.RunCommand(command, environ, params.Clock, params.Timeout) activationResult := ActivationResult{ - Stderr: result.Stderr, - Stdout: result.Stdout, + Stderr: string(result.Stderr), + Stdout: string(result.Stdout), Code: result.Code, } diff --git a/network/netplan/activate_test.go b/network/netplan/activate_test.go index 5c026dbd514..1e6adf037c4 100644 --- a/network/netplan/activate_test.go +++ b/network/netplan/activate_test.go @@ -11,6 +11,7 @@ import ( gc "gopkg.in/check.v1" "github.com/juju/juju/network/netplan" + coretesting "github.com/juju/juju/testing" ) type ActivateSuite struct { @@ -39,6 +40,7 @@ func (s *ActivateSuite) TestNoDirectory(c *gc.C) { } func (s *ActivateSuite) TestActivateSuccess(c *gc.C) { + coretesting.SkipIfWindowsBug(c, "lp:1771077") tempDir := c.MkDir() params := netplan.ActivationParams{ Devices: []netplan.DeviceToBridge{ @@ -70,7 +72,41 @@ func (s *ActivateSuite) TestActivateSuccess(c *gc.C) { c.Check(err, jc.ErrorIsNil) } +func (s *ActivateSuite) TestActivateDeviceAndVLAN(c *gc.C) { + coretesting.SkipIfWindowsBug(c, "lp:1771077") + tempDir := c.MkDir() + params := netplan.ActivationParams{ + Devices: []netplan.DeviceToBridge{ + { + DeviceName: "eno1", + MACAddress: "00:11:22:33:44:99", // That's a wrong MAC, we should fall back to name + BridgeName: "br-eno1", + }, + { + DeviceName: "eno1.123", + MACAddress: "00:11:22:33:44:99", + BridgeName: "br-eno1.123", + }, + }, + Directory: tempDir, + RunPrefix: "exit 0 &&", + } + files := []string{"00.yaml", "01.yaml"} + contents := make([][]byte, len(files)) + for i, file := range files { + var err error + contents[i], err = ioutil.ReadFile(path.Join("testdata/TestReadWriteBackup", file)) + c.Assert(err, jc.ErrorIsNil) + err = ioutil.WriteFile(path.Join(tempDir, file), contents[i], 0644) + c.Assert(err, jc.ErrorIsNil) + } + result, err := netplan.BridgeAndActivate(params) + c.Check(result, gc.IsNil) + c.Check(err, jc.ErrorIsNil) +} + func (s *ActivateSuite) TestActivateFailure(c *gc.C) { + coretesting.SkipIfWindowsBug(c, "lp:1771077") tempDir := c.MkDir() params := netplan.ActivationParams{ Devices: []netplan.DeviceToBridge{ @@ -99,8 +135,8 @@ func (s *ActivateSuite) TestActivateFailure(c *gc.C) { } result, err := netplan.BridgeAndActivate(params) c.Assert(result, gc.NotNil) - c.Check(result.Stdout, gc.DeepEquals, []byte("This is stdout")) - c.Check(result.Stderr, gc.DeepEquals, []byte("This is stderr")) + c.Check(string(result.Stdout), gc.DeepEquals, "This is stdout") + c.Check(string(result.Stderr), gc.DeepEquals, "This is stderr") c.Check(result.Code, gc.Equals, 1) c.Check(err, gc.ErrorMatches, "bridge activation error code 1") @@ -124,6 +160,7 @@ func (s *ActivateSuite) TestActivateFailure(c *gc.C) { } func (s *ActivateSuite) TestActivateTimeout(c *gc.C) { + // coretesting.SkipIfWindowsBug(c, "lp:1771077") tempDir := c.MkDir() params := netplan.ActivationParams{ Devices: []netplan.DeviceToBridge{ @@ -153,6 +190,6 @@ func (s *ActivateSuite) TestActivateTimeout(c *gc.C) { c.Assert(err, jc.ErrorIsNil) } result, err := netplan.BridgeAndActivate(params) - c.Assert(result, gc.NotNil) + c.Check(result, gc.NotNil) c.Check(err, gc.ErrorMatches, "bridge activation error: command cancelled") } diff --git a/network/netplan/netplan.go b/network/netplan/netplan.go index 56c45f2995d..28b21cc6620 100644 --- a/network/netplan/netplan.go +++ b/network/netplan/netplan.go @@ -20,18 +20,32 @@ type Nameservers struct { Search []string `yaml:"search,omitempty,flow"` Addresses []string `yaml:"addresses,omitempty,flow"` } + +// Interface includes all the fields that are common between all interfaces (ethernet, wifi, bridge, bond) type Interface struct { - Dhcp4 bool `yaml:"dhcp4,omitempty"` - Dhcp6 bool `yaml:"dhcp6,omitempty"` - Addresses []string `yaml:"addresses,omitempty"` - Gateway4 string `yaml:"gateway4,omitempty"` - Gateway6 string `yaml:"gateway6,omitempty"` - Nameservers Nameservers `yaml:"nameservers,omitempty"` - MTU int `yaml:"mtu,omitempty"` - Routes []Route `yaml:"routes,omitempty"` + AcceptRA *bool `yaml:"accept-ra,omitempty"` + Addresses []string `yaml:"addresses,omitempty"` + // Critical doesn't have to be *bool because it is only used if True + Critical bool `yaml:"critical,omitempty"` + // DHCP4 defaults to true, so we must use a pointer to know if it was specified as false + DHCP4 *bool `yaml:"dhcp4,omitempty"` + DHCP6 *bool `yaml:"dhcp6,omitempty"` + DHCPIdentifier string `yaml:"dhcp-identifier,omitempty"` // "duid" or "mac" + Gateway4 string `yaml:"gateway4,omitempty"` + Gateway6 string `yaml:"gateway6,omitempty"` + Nameservers Nameservers `yaml:"nameservers,omitempty"` + MACAddress string `yaml:"macaddress,omitempty"` + MTU int `yaml:"mtu,omitempty"` + Renderer string `yaml:"renderer,omitempty"` // NetworkManager or networkd + Routes []Route `yaml:"routes,omitempty"` + RoutingPolicy []RoutePolicy `yaml:"routing-policy,omitempty"` + // Optional doesn't have to be *bool because it is only used if True + Optional bool `yaml:"optional,omitempty"` } + +// Ethernet defines fields for just Ethernet devices type Ethernet struct { - Match map[string]string `yaml:"match"` + Match map[string]string `yaml:"match,omitempty"` Wakeonlan bool `yaml:"wakeonlan,omitempty"` SetName string `yaml:"set-name,omitempty"` Interface `yaml:",inline"` @@ -44,19 +58,46 @@ type AccessPoint struct { type Wifi struct { Match map[string]string `yaml:"match,omitempty"` SetName string `yaml:"set-name,omitempty"` + Wakeonlan bool `yaml:"wakeonlan,omitempty"` AccessPoints map[string]AccessPoint `yaml:"access-points,omitempty"` Interface `yaml:",inline"` } +type BridgeParameters struct { + AgeingTime *int `yaml:"ageing-time,omitempty"` + ForwardDelay *int `yaml:"forward-delay,omitempty"` + HelloTime *int `yaml:"hello-time,omitempty"` + MaxAge *int `yaml:"max-age,omitempty"` + PathCost map[string]int `yaml:"path-cost,omitempty"` + PortPriority map[string]int `yaml:"port-priority,omitempty"` + Priority *int `yaml:"priority,omitempty"` + STP *bool `yaml:"stp,omitempty"` +} + type Bridge struct { Interfaces []string `yaml:"interfaces,omitempty,flow"` Interface `yaml:",inline"` + Parameters BridgeParameters `yaml:"parameters,omitempty"` } type Route struct { + From string `yaml:"from,omitempty"` + OnLink *bool `yaml:"on-link,omitempty"` + Scope string `yaml:"scope,omitempty"` + Table *int `yaml:"table,omitempty"` To string `yaml:"to,omitempty"` + Type string `yaml:"type,omitempty"` Via string `yaml:"via,omitempty"` - Metric int `yaml:"metric,omitempty"` + Metric *int `yaml:"metric,omitempty"` +} + +type RoutePolicy struct { + From string `yaml:"from,omitempty"` + Mark *int `yaml:"mark,omitempty"` + Priority *int `yaml:"priority,omitempty"` + Table *int `yaml:"table,omitempty"` + To string `yaml:"to,omitempty"` + TypeOfService *int `yaml:"type-of-service,omitempty"` } type Network struct { @@ -65,6 +106,8 @@ type Network struct { Ethernets map[string]Ethernet `yaml:"ethernets,omitempty"` Wifis map[string]Wifi `yaml:"wifis,omitempty"` Bridges map[string]Bridge `yaml:"bridges,omitempty"` + Bonds map[string]Bond `yaml:"bonds,omitempty"` + VLANs map[string]VLAN `yaml:"vlans,omitempty"` Routes []Route `yaml:"routes,omitempty"` } @@ -76,41 +119,168 @@ type Netplan struct { writtenFile string } -// BridgeDeviceById takes a deviceId and creates a bridge with this device +// VLAN represents the structures for defining VLAN sections +type VLAN struct { + Id *int `yaml:"id,omitempty"` + Link string `yaml:"link,omitempty"` + Interface `yaml:",inline"` +} + +// Bond is the interface definition of the bonds: section of netplan +type Bond struct { + Interfaces []string `yaml:"interfaces,omitempty,flow"` + Interface `yaml:",inline"` + Parameters BondParameters `yaml:"parameters,omitempty"` +} + +// IntString is used to specialize values that can be integers or strings +type IntString struct { + Int *int + String *string +} + +func (i *IntString) UnmarshalYAML(unmarshal func(interface{}) error) error { + var asInt int + var err error + if err = unmarshal(&asInt); err == nil { + i.Int = &asInt + return nil + } + var asString string + if err = unmarshal(&asString); err == nil { + i.String = &asString + return nil + } + return errors.Annotatef(err, "not valid as an int or a string") +} + +func (i IntString) MarshalYAML() (interface{}, error) { + if i.Int != nil { + return *i.Int, nil + } else if i.String != nil { + return *i.String, nil + } + return nil, nil +} + +// For a definition of what netplan supports see here: +// https://github.com/CanonicalLtd/netplan/blob/7afef6af053794a400d96f89a81c938c08420783/src/parse.c#L1180 +// For a definition of what the parameters mean or what values they can contain, see here: +// https://www.kernel.org/doc/Documentation/networking/bonding.txt +// Note that most parameters can be specified as integers or as strings, which you need to be careful with YAML +// as it defaults to strongly typing them. +// TODO: (jam 2018-05-14) Should we be sorting the attributes alphabetically? +type BondParameters struct { + Mode IntString `yaml:"mode,omitempty"` + LACPRate IntString `yaml:"lacp-rate,omitempty"` + MIIMonitorInterval *int `yaml:"mii-monitor-interval,omitempty"` + MinLinks *int `yaml:"min-links,omitempty"` + TransmitHashPolicy string `yaml:"transmit-hash-policy,omitempty"` + ADSelect IntString `yaml:"ad-select,omitempty"` + AllSlavesActive *bool `yaml:"all-slaves-active,omitempty"` + ARPInterval *int `yaml:"arp-interval,omitempty"` + ARPIPTargets []string `yaml:"arp-ip-targets,omitempty"` + ARPValidate IntString `yaml:"arp-validate,omitempty"` + ARPAllTargets IntString `yaml:"arp-all-targets,omitempty"` + UpDelay *int `yaml:"up-delay,omitempty"` + DownDelay *int `yaml:"down-delay,omitempty"` + FailOverMACPolicy IntString `yaml:"fail-over-mac-policy,omitempty"` + // Netplan misspelled this as 'gratuitious-arp', not sure if it works with that name. + // We may need custom handling of both spellings. + GratuitousARP *int `yaml:"gratuitious-arp,omitempty"` + PacketsPerSlave *int `yaml:"packets-per-slave,omitempty"` + PrimaryReselectPolicy IntString `yaml:"primary-reselect-policy,omitempty"` + ResendIGMP *int `yaml:"resend-igmp,omitempty"` + // bonding.txt says that this can be a value from 1-0x7fffffff, should we be forcing it to be a hex value? + LearnPacketInterval *int `yaml:"learn-packet-interval,omitempty"` + Primary string `yaml:"primary,omitempty"` +} + +// BridgeEthernetById takes a deviceId and creates a bridge with this device // using this devices config func (np *Netplan) BridgeEthernetById(deviceId string, bridgeName string) (err error) { ethernet, ok := np.Network.Ethernets[deviceId] if !ok { - return errors.NotFoundf("Device with id %q for bridge %q", deviceId, bridgeName) + return errors.NotFoundf("ethernet device with id %q for bridge %q", deviceId, bridgeName) + } + shouldCreate, err := np.shouldCreateBridge(deviceId, bridgeName) + if !shouldCreate { + // err may be nil, but we shouldn't continue creating + return errors.Trace(err) + } + np.createBridgeFromInterface(bridgeName, deviceId, ðernet.Interface) + np.Network.Ethernets[deviceId] = ethernet + return nil +} + +// BridgeVLANById takes a deviceId and creates a bridge with this device +// using this devices config +func (np *Netplan) BridgeVLANById(deviceId string, bridgeName string) (err error) { + vlan, ok := np.Network.VLANs[deviceId] + if !ok { + return errors.NotFoundf("VLAN device with id %q for bridge %q", deviceId, bridgeName) + } + shouldCreate, err := np.shouldCreateBridge(deviceId, bridgeName) + if !shouldCreate { + // err may be nil, but we shouldn't continue creating + return errors.Trace(err) + } + np.createBridgeFromInterface(bridgeName, deviceId, &vlan.Interface) + np.Network.VLANs[deviceId] = vlan + return nil +} + +// BridgeBondById takes a deviceId and creates a bridge with this device +// using this devices config +func (np *Netplan) BridgeBondById(deviceId string, bridgeName string) (err error) { + bond, ok := np.Network.Bonds[deviceId] + if !ok { + return errors.NotFoundf("bond device with id %q for bridge %q", deviceId, bridgeName) } + shouldCreate, err := np.shouldCreateBridge(deviceId, bridgeName) + if !shouldCreate { + // err may be nil, but we shouldn't continue creating + return errors.Trace(err) + } + np.createBridgeFromInterface(bridgeName, deviceId, &bond.Interface) + np.Network.Bonds[deviceId] = bond + return nil +} + +// shouldCreateBridge returns true only if it is clear the bridge doesn't already exist, and that the existing device +// isn't in a different bridge. +func (np *Netplan) shouldCreateBridge(deviceId string, bridgeName string) (bool, error) { for bName, bridge := range np.Network.Bridges { for _, i := range bridge.Interfaces { if i == deviceId { - // The device is already properly bridged, we're not doing anything + // The device is already properly bridged, nothing to do if bridgeName == bName { - return nil + return false, nil } else { - return errors.AlreadyExistsf("Device %q is already bridged in bridge %q instead of %q", deviceId, bName, bridgeName) + return false, errors.AlreadyExistsf("cannot create bridge %q, device %q in bridge %q", bridgeName, deviceId, bName) } } } if bridgeName == bName { - return errors.AlreadyExistsf("Cannot bridge device %q on bridge %q - bridge named %q", deviceId, bridgeName, bridgeName) + return false, errors.AlreadyExistsf( + "cannot create bridge %q with device %q - bridge %q w/ interfaces %q", + bridgeName, deviceId, bridgeName, strings.Join(bridge.Interfaces, ", ")) } } - // copy aside and clear the IP settings from the original Ethernet device, except for MTU - intf := ethernet.Interface - ethernet.Interface = Interface{MTU: intf.MTU} - // create a bridge + return true, nil +} + +// createBridgeFromInterface will create a bridge stealing the interface details, and wiping the existing interface +// except for MTU so that IP Address information is never duplicated. +func (np *Netplan) createBridgeFromInterface(bridgeName, deviceId string, intf *Interface) { if np.Network.Bridges == nil { np.Network.Bridges = make(map[string]Bridge) } np.Network.Bridges[bridgeName] = Bridge{ Interfaces: []string{deviceId}, - Interface: intf, + Interface: *intf, } - np.Network.Ethernets[deviceId] = ethernet - return nil + *intf = Interface{MTU: intf.MTU} } func Unmarshal(in []byte, out interface{}) (err error) { @@ -254,8 +424,11 @@ func (np *Netplan) FindEthernetByMAC(mac string) (device string, err error) { if v, ok := ethernet.Match["macaddress"]; ok && v == mac { return id, nil } + if ethernet.MACAddress == mac { + return id, nil + } } - return "", errors.NotFoundf("Ethernet device with mac %q", mac) + return "", errors.NotFoundf("Ethernet device with MAC %q", mac) } func (np *Netplan) FindEthernetByName(name string) (device string, err error) { @@ -270,5 +443,90 @@ func (np *Netplan) FindEthernetByName(name string) (device string, err error) { return id, nil } } + if _, ok := np.Network.Ethernets[name]; ok { + return name, nil + } return "", errors.NotFoundf("Ethernet device with name %q", name) } + +func (np *Netplan) FindVLANByMAC(mac string) (device string, err error) { + for id, vlan := range np.Network.VLANs { + if vlan.MACAddress == mac { + return id, nil + } + } + return "", errors.NotFoundf("VLAN device with MAC %q", mac) +} + +func (np *Netplan) FindVLANByName(name string) (device string, err error) { + if _, ok := np.Network.VLANs[name]; ok { + return name, nil + } + return "", errors.NotFoundf("VLAN device with name %q", name) +} + +func (np *Netplan) FindBondByMAC(mac string) (device string, err error) { + for id, bonds := range np.Network.Bonds { + if bonds.MACAddress == mac { + return id, nil + } + } + return "", errors.NotFoundf("bond device with MAC %q", mac) +} + +func (np *Netplan) FindBondByName(name string) (device string, err error) { + if _, ok := np.Network.Bonds[name]; ok { + return name, nil + } + return "", errors.NotFoundf("bond device with name %q", name) +} + +type DeviceType string + +const ( + TypeEthernet = DeviceType("ethernet") + TypeVLAN = DeviceType("vlan") + TypeBond = DeviceType("bond") +) + +// FindDeviceByMACOrName will look for an Ethernet, VLAN or Bond matching the Name of the device or its MAC address. +// Name is preferred to MAC address. +func (np *Netplan) FindDeviceByNameOrMAC(name, mac string) (string, DeviceType, error) { + if name != "" { + bond, err := np.FindBondByName(name) + if err == nil { + return bond, TypeBond, nil + } + if !errors.IsNotFound(err) { + return "", "", errors.Trace(err) + } + vlan, err := np.FindVLANByName(name) + if err == nil { + return vlan, TypeVLAN, nil + } + ethernet, err := np.FindEthernetByName(name) + if err == nil { + return ethernet, TypeEthernet, nil + } + + } + // by MAC is less reliable because things like vlans often have the same MAC address + if mac != "" { + bond, err := np.FindBondByMAC(mac) + if err == nil { + return bond, TypeBond, nil + } + if !errors.IsNotFound(err) { + return "", "", errors.Trace(err) + } + vlan, err := np.FindVLANByMAC(mac) + if err == nil { + return vlan, TypeVLAN, nil + } + ethernet, err := np.FindEthernetByMAC(mac) + if err == nil { + return ethernet, TypeEthernet, nil + } + } + return "", "", errors.NotFoundf("device - name %q MAC %q", name, mac) +} diff --git a/network/netplan/netplan_test.go b/network/netplan/netplan_test.go index 1f7028c0a69..34a6118526c 100644 --- a/network/netplan/netplan_test.go +++ b/network/netplan/netplan_test.go @@ -6,13 +6,17 @@ import ( "math/rand" "os" "path" + "reflect" "strings" + "github.com/juju/errors" "github.com/juju/testing" jc "github.com/juju/testing/checkers" + "github.com/kr/pretty" gc "gopkg.in/check.v1" "github.com/juju/juju/network/netplan" + coretesting "github.com/juju/juju/testing" ) type NetplanSuite struct { @@ -21,8 +25,30 @@ type NetplanSuite struct { var _ = gc.Suite(&NetplanSuite{}) +func MustNetplanFromYaml(c *gc.C, input string) *netplan.Netplan { + var np netplan.Netplan + if strings.HasPrefix(input, "\n") { + input = input[1:] + } + err := netplan.Unmarshal([]byte(input), &np) + c.Assert(err, jc.ErrorIsNil) + return &np +} + +func checkNetplanRoundTrips(c *gc.C, input string) { + if strings.HasPrefix(input, "\n") { + input = input[1:] + } + var np netplan.Netplan + err := netplan.Unmarshal([]byte(input), &np) + c.Assert(err, jc.ErrorIsNil) + out, err := netplan.Marshal(np) + c.Assert(err, jc.ErrorIsNil) + c.Check(string(out), gc.Equals, input) +} + func (s *NetplanSuite) TestStructures(c *gc.C) { - input := ` + checkNetplanRoundTrips(c, ` network: version: 2 renderer: NetworkManager @@ -31,10 +57,12 @@ network: match: macaddress: "00:11:22:33:44:55" wakeonlan: true - dhcp4: true addresses: - 192.168.14.2/24 - 2001:1::1/64 + critical: true + dhcp4: true + dhcp-identifier: mac gateway4: 192.168.14.1 gateway6: 2001:1::2 nameservers: @@ -66,22 +94,373 @@ network: bridges: br0: interfaces: [wlp1s0, switchports] - dhcp4: true + dhcp4: false routes: - to: 0.0.0.0/0 via: 11.0.0.1 metric: 3 -`[1:] - var np netplan.Netplan - err := netplan.Unmarshal([]byte(input), &np) - c.Check(err, jc.ErrorIsNil) - out, err := netplan.Marshal(np) - c.Check(err, jc.ErrorIsNil) - c.Check(string(out), gc.Equals, input) +`) +} + +func (s *NetplanSuite) TestBasicBond(c *gc.C) { + checkNetplanRoundTrips(c, ` +network: + version: 2 + renderer: NetworkManager + ethernets: + id0: + match: + macaddress: "00:11:22:33:44:55" + set-name: id0 + id1: + match: + macaddress: de:ad:be:ef:01:02 + set-name: id1 + bridges: + br-bond0: + interfaces: [bond0] + dhcp4: true + bonds: + bond0: + interfaces: [id0, id1] + parameters: + mode: 802.3ad + lacp-rate: fast + mii-monitor-interval: 100 + transmit-hash-policy: layer2 + up-delay: 0 + down-delay: 0 +`) +} + +func (s *NetplanSuite) TestParseBridgedBond(c *gc.C) { + checkNetplanRoundTrips(c, ` +network: + version: 2 + renderer: NetworkManager + ethernets: + id0: + match: + macaddress: "00:11:22:33:44:55" + set-name: id0 + id1: + match: + macaddress: de:ad:be:ef:01:02 + set-name: id1 + bridges: + br-bond0: + interfaces: [bond0] + dhcp4: true + bonds: + bond0: + interfaces: [id0, id1] + parameters: + mode: 802.3ad + lacp-rate: fast + mii-monitor-interval: 100 + transmit-hash-policy: layer2 + up-delay: 0 + down-delay: 0 +`) +} + +func (s *NetplanSuite) TestBondsIntParameters(c *gc.C) { + // several parameters can be specified as an integer or a string + // such as 'mode: 0' is the same as 'balance-rr' + checkNetplanRoundTrips(c, ` +network: + version: 2 + renderer: NetworkManager + ethernets: + id0: + match: + macaddress: "00:11:22:33:44:55" + set-name: id0 + id1: + match: + macaddress: de:ad:be:ef:01:02 + set-name: id1 + bonds: + bond0: + interfaces: [id0, id1] + parameters: + mode: 0 + lacp-rate: 1 + ad-select: 1 + all-slaves-active: true + arp-validate: 0 + arp-all-targets: 0 + fail-over-mac-policy: 1 + primary-reselect-policy: 1 +`) + checkNetplanRoundTrips(c, ` +network: + version: 2 + renderer: NetworkManager + ethernets: + id0: + match: + macaddress: "00:11:22:33:44:55" + set-name: id0 + id1: + match: + macaddress: de:ad:be:ef:01:02 + set-name: id1 + bonds: + bond0: + interfaces: [id0, id1] + parameters: + mode: balance-rr + lacp-rate: fast + ad-select: bandwidth + all-slaves-active: false + arp-validate: filter + arp-all-targets: all + fail-over-mac-policy: follow + primary-reselect-policy: always +`) +} + +func (s *NetplanSuite) TestBondWithVLAN(c *gc.C) { + checkNetplanRoundTrips(c, ` +network: + version: 2 + renderer: NetworkManager + ethernets: + id0: + match: + macaddress: "00:11:22:33:44:55" + set-name: id0 + id1: + match: + macaddress: de:ad:be:ef:01:02 + set-name: id1 + bonds: + bond0: + interfaces: [id0, id1] + parameters: + mode: 802.3ad + lacp-rate: fast + mii-monitor-interval: 100 + transmit-hash-policy: layer2 + up-delay: 0 + down-delay: 0 + vlans: + bond0.209: + id: 209 + link: bond0 + addresses: + - 123.123.123.123/24 + nameservers: + addresses: [8.8.8.8] +`) +} + +func (s *NetplanSuite) TestBondsAllParameters(c *gc.C) { + // All parameters don't inherently make sense at the same time, but we should be able to parse all of them. + checkNetplanRoundTrips(c, ` +network: + version: 2 + renderer: NetworkManager + ethernets: + id0: + match: + macaddress: "00:11:22:33:44:55" + set-name: id0 + id1: + match: + macaddress: de:ad:be:ef:01:02 + set-name: id1 + id2: + match: + macaddress: de:ad:be:ef:01:03 + id3: + match: + macaddress: de:ad:be:ef:01:04 + bonds: + bond0: + interfaces: [id0, id1] + parameters: + mode: 802.3ad + lacp-rate: fast + mii-monitor-interval: 100 + min-links: 0 + transmit-hash-policy: layer2 + ad-select: 1 + all-slaves-active: true + arp-interval: 100 + arp-ip-targets: + - 192.168.0.1 + - 192.168.10.20 + arp-validate: none + arp-all-targets: all + up-delay: 0 + down-delay: 0 + fail-over-mac-policy: follow + gratuitious-arp: 0 + packets-per-slave: 0 + primary-reselect-policy: better + resend-igmp: 0 + learn-packet-interval: 4660 + primary: id1 +`) +} + +func (s *NetplanSuite) TestBridgesAllParameters(c *gc.C) { + // All parameters don't inherently make sense at the same time, but we should be able to parse all of them. + checkNetplanRoundTrips(c, ` +network: + version: 2 + renderer: NetworkManager + ethernets: + id0: + match: + macaddress: "00:11:22:33:44:55" + set-name: id0 + id1: + match: + macaddress: de:ad:be:ef:01:02 + set-name: id1 + id2: + match: + macaddress: de:ad:be:ef:01:03 + set-name: id2 + bridges: + br-id0: + interfaces: [id0] + accept-ra: true + addresses: + - 123.123.123.123/24 + dhcp4: false + dhcp6: true + dhcp-identifier: duid + parameters: + ageing-time: 0 + forward-delay: 0 + hello-time: 0 + max-age: 0 + path-cost: + id0: 0 + port-priority: + id0: 0 + priority: 0 + stp: false + br-id1: + interfaces: [id1] + accept-ra: false + addresses: + - 2001::1/64 + dhcp4: true + dhcp6: true + dhcp-identifier: mac + parameters: + ageing-time: 100 + forward-delay: 10 + hello-time: 20 + max-age: 10 + path-cost: + id1: 50 + port-priority: + id1: 50 + priority: 20000 + stp: true + br-id2: + interfaces: [id2] + br-id3: + interfaces: [id2] + parameters: + ageing-time: 10 +`) +} + +func (s *NetplanSuite) TestAllRoutesParams(c *gc.C) { + checkNetplanRoundTrips(c, ` +network: + version: 2 + renderer: NetworkManager + ethernets: + id0: + match: + macaddress: "00:11:22:33:44:55" + set-name: id0 + routes: + - from: 192.168.0.0/24 + on-link: true + scope: global + table: 1234 + to: 192.168.3.1/24 + type: unicast + via: 192.168.3.1 + metric: 1234567 + - on-link: false + to: 192.168.5.1/24 + via: 192.168.5.1 + metric: 0 + - to: 192.168.5.1/24 + type: unreachable + via: 192.168.5.1 + routing-policy: + - from: 192.168.10.0/24 + mark: 123 + priority: 10 + table: 1234 + to: 192.168.3.1/24 + type-of-service: 0 + - from: 192.168.12.0/24 + mark: 0 + priority: 0 + table: 0 + to: 192.168.3.1/24 + type-of-service: 255 +`) +} + +func (s *NetplanSuite) TestAllVLANParams(c *gc.C) { + checkNetplanRoundTrips(c, ` +network: + version: 2 + renderer: NetworkManager + ethernets: + id0: + match: + macaddress: "00:11:22:33:44:55" + set-name: id0 + vlans: + id0.123: + id: 123 + link: id0 + accept-ra: true + addresses: + - 123.123.123.123/24 + critical: true + dhcp4: false + dhcp6: false + dhcp-identifier: duid + gateway4: 123.123.123.123 + gateway6: dead::beef + nameservers: + addresses: [8.8.8.8] + macaddress: de:ad:be:ef:12:34 + mtu: 9000 + renderer: NetworkManager + routes: + - table: 102 + to: 100.0.0.0/8 + via: 1.2.3.10 + metric: 5 + routing-policy: + - from: 192.168.5.0/24 + table: 103 + optional: true + id0.456: + id: 456 + link: id0 + accept-ra: false +`) } func (s *NetplanSuite) TestSimpleBridger(c *gc.C) { - input := ` + np := MustNetplanFromYaml(c, ` network: version: 2 renderer: NetworkManager @@ -101,7 +480,7 @@ network: - to: 100.0.0.0/8 via: 1.2.3.10 metric: 5 -`[1:] +`) expected := ` network: version: 2 @@ -126,13 +505,8 @@ network: via: 1.2.3.10 metric: 5 `[1:] - var np netplan.Netplan - - err := netplan.Unmarshal([]byte(input), &np) - c.Check(err, jc.ErrorIsNil) - - err = np.BridgeEthernetById("id0", "juju-bridge") - c.Check(err, jc.ErrorIsNil) + err := np.BridgeEthernetById("id0", "juju-bridge") + c.Assert(err, jc.ErrorIsNil) out, err := netplan.Marshal(np) c.Assert(err, jc.ErrorIsNil) @@ -164,17 +538,15 @@ network: via: 1.2.3.10 metric: 5 `[1:] - var np netplan.Netplan - err := netplan.Unmarshal([]byte(input), &np) - c.Check(err, jc.ErrorIsNil) - err = np.BridgeEthernetById("id0", "juju-bridge") - c.Check(err, jc.ErrorIsNil) + np := MustNetplanFromYaml(c, input) + c.Assert(np.BridgeEthernetById("id0", "juju-bridge"), jc.ErrorIsNil) out, err := netplan.Marshal(np) c.Check(string(out), gc.Equals, input) + c.Assert(err, jc.ErrorIsNil) } func (s *NetplanSuite) TestBridgerBridgeExists(c *gc.C) { - input := ` + np := MustNetplanFromYaml(c, ` network: version: 2 renderer: NetworkManager @@ -204,16 +576,13 @@ network: nameservers: search: [foo.local, bar.local] addresses: [8.8.8.8] -`[1:] - var np netplan.Netplan - err := netplan.Unmarshal([]byte(input), &np) - c.Check(err, jc.ErrorIsNil) - err = np.BridgeEthernetById("id0", "juju-bridge") - c.Check(err, gc.ErrorMatches, `Cannot bridge device "id0" on bridge "juju-bridge" - bridge named "juju-bridge" already exists`) +`) + err := np.BridgeEthernetById("id0", "juju-bridge") + c.Check(err, gc.ErrorMatches, `cannot create bridge "juju-bridge" with device "id0" - bridge "juju-bridge" w/ interfaces "id1" already exists`) } func (s *NetplanSuite) TestBridgerDeviceBridged(c *gc.C) { - input := ` + np := MustNetplanFromYaml(c, ` network: version: 2 renderer: NetworkManager @@ -240,16 +609,39 @@ network: nameservers: search: [foo.local, bar.local] addresses: [8.8.8.8] -`[1:] - var np netplan.Netplan - err := netplan.Unmarshal([]byte(input), &np) - c.Check(err, jc.ErrorIsNil) - err = np.BridgeEthernetById("id0", "juju-bridge") - c.Check(err, gc.ErrorMatches, `.*Device "id0" is already bridged in bridge "not-juju-bridge" instead of "juju-bridge".*`) +`) + err := np.BridgeEthernetById("id0", "juju-bridge") + c.Check(err, gc.ErrorMatches, `cannot create bridge "juju-bridge", device "id0" in bridge "not-juju-bridge" already exists`) } -func (s *NetplanSuite) TestBridgerDeviceMissing(c *gc.C) { - input := ` +func (s *NetplanSuite) TestBridgerEthernetMissing(c *gc.C) { + np := MustNetplanFromYaml(c, ` +network: + version: 2 + renderer: NetworkManager + ethernets: + id0: + match: + macaddress: "00:11:22:33:44:55" + bridges: + not-juju-bridge: + interfaces: [id0] + addresses: + - 1.2.3.4/24 + - 2000::1/64 + gateway4: 1.2.3.5 + gateway6: 2000::2 + nameservers: + search: [foo.local, bar.local] + addresses: [8.8.8.8] +`) + err := np.BridgeEthernetById("id7", "juju-bridge") + c.Check(err, gc.ErrorMatches, `ethernet device with id "id7" for bridge "juju-bridge" not found`) + c.Check(err, jc.Satisfies, errors.IsNotFound) +} + +func (s *NetplanSuite) TestBridgeVLAN(c *gc.C) { + np := MustNetplanFromYaml(c, ` network: version: 2 renderer: NetworkManager @@ -257,14 +649,74 @@ network: id0: match: macaddress: "00:11:22:33:44:55" + vlans: + id0.1234: + link: id0 + id: 1234 addresses: - 1.2.3.4/24 - 2000::1/64 gateway4: 1.2.3.5 gateway6: 2000::2 + macaddress: "00:11:22:33:44:55" nameservers: search: [foo.local, bar.local] addresses: [8.8.8.8] + routes: + - to: 100.0.0.0/8 + via: 1.2.3.10 + metric: 5 +`) + expected := ` +network: + version: 2 + renderer: NetworkManager + ethernets: + id0: + match: + macaddress: "00:11:22:33:44:55" + bridges: + br-id0.1234: + interfaces: [id0.1234] + addresses: + - 1.2.3.4/24 + - 2000::1/64 + gateway4: 1.2.3.5 + gateway6: 2000::2 + nameservers: + search: [foo.local, bar.local] + addresses: [8.8.8.8] + macaddress: "00:11:22:33:44:55" + routes: + - to: 100.0.0.0/8 + via: 1.2.3.10 + metric: 5 + vlans: + id0.1234: + id: 1234 + link: id0 +`[1:] + err := np.BridgeVLANById("id0.1234", "br-id0.1234") + c.Assert(err, jc.ErrorIsNil) + + out, err := netplan.Marshal(np) + c.Assert(err, jc.ErrorIsNil) + c.Check(string(out), gc.Equals, expected) +} + +func (s *NetplanSuite) TestBridgerVLANMissing(c *gc.C) { + np := MustNetplanFromYaml(c, ` +network: + version: 2 + renderer: NetworkManager + ethernets: + id0: + match: + macaddress: "00:11:22:33:44:55" + vlans: + id0.1234: + link: id0 + id: 1234 bridges: not-juju-bridge: interfaces: [id0] @@ -276,16 +728,194 @@ network: nameservers: search: [foo.local, bar.local] addresses: [8.8.8.8] +`) + err := np.BridgeVLANById("id0.1235", "br-id0.1235") + c.Check(err, gc.ErrorMatches, `VLAN device with id "id0.1235" for bridge "br-id0.1235" not found`) + c.Check(err, jc.Satisfies, errors.IsNotFound) +} + +func (s *NetplanSuite) TestBridgeVLANAndLinkedDevice(c *gc.C) { + np := MustNetplanFromYaml(c, ` +network: + version: 2 + renderer: NetworkManager + ethernets: + id0: + match: + macaddress: "00:11:22:33:44:55" + addresses: + - 2.3.4.5/24 + macaddress: "00:11:22:33:44:55" + vlans: + id0.1234: + link: id0 + id: 1234 + addresses: + - 1.2.3.4/24 + - 2000::1/64 + gateway4: 1.2.3.5 + gateway6: 2000::2 + macaddress: "00:11:22:33:44:55" + nameservers: + search: [foo.local, bar.local] + addresses: [8.8.8.8] + routes: + - to: 100.0.0.0/8 + via: 1.2.3.10 + metric: 5 +`) + expected := ` +network: + version: 2 + renderer: NetworkManager + ethernets: + id0: + match: + macaddress: "00:11:22:33:44:55" + bridges: + br-id0: + interfaces: [id0] + addresses: + - 2.3.4.5/24 + macaddress: "00:11:22:33:44:55" + br-id0.1234: + interfaces: [id0.1234] + addresses: + - 1.2.3.4/24 + - 2000::1/64 + gateway4: 1.2.3.5 + gateway6: 2000::2 + nameservers: + search: [foo.local, bar.local] + addresses: [8.8.8.8] + macaddress: "00:11:22:33:44:55" + routes: + - to: 100.0.0.0/8 + via: 1.2.3.10 + metric: 5 + vlans: + id0.1234: + id: 1234 + link: id0 `[1:] - var np netplan.Netplan - err := netplan.Unmarshal([]byte(input), &np) - c.Check(err, jc.ErrorIsNil) - err = np.BridgeEthernetById("id7", "juju-bridge") - c.Check(err, gc.ErrorMatches, `Device with id "id7" for bridge "juju-bridge" not found`) + err := np.BridgeEthernetById("id0", "br-id0") + c.Assert(err, jc.ErrorIsNil) + err = np.BridgeVLANById("id0.1234", "br-id0.1234") + c.Assert(err, jc.ErrorIsNil) + + out, err := netplan.Marshal(np) + c.Assert(err, jc.ErrorIsNil) + c.Check(string(out), gc.Equals, expected) } -func (s *NetplanSuite) TestFindEthernetBySetName(c *gc.C) { - input := ` +func (s *NetplanSuite) TestBridgeBond(c *gc.C) { + np := MustNetplanFromYaml(c, ` +network: + version: 2 + renderer: NetworkManager + ethernets: + id0: + match: + macaddress: de:ad:22:33:44:55 + id1: + match: + macaddress: de:ad:22:33:44:66 + bonds: + bond0: + interfaces: [id0, id1] + addresses: + - 1.2.3.4/24 + - 2000::1/64 + gateway4: 1.2.3.5 + gateway6: 2000::2 + nameservers: + search: [foo.local, bar.local] + addresses: [8.8.8.8] + routes: + - to: 100.0.0.0/8 + via: 1.2.3.10 + metric: 5 + parameters: + lacp-rate: fast +`) + expected := ` +network: + version: 2 + renderer: NetworkManager + ethernets: + id0: + match: + macaddress: de:ad:22:33:44:55 + id1: + match: + macaddress: de:ad:22:33:44:66 + bridges: + br-bond0: + interfaces: [bond0] + addresses: + - 1.2.3.4/24 + - 2000::1/64 + gateway4: 1.2.3.5 + gateway6: 2000::2 + nameservers: + search: [foo.local, bar.local] + addresses: [8.8.8.8] + routes: + - to: 100.0.0.0/8 + via: 1.2.3.10 + metric: 5 + bonds: + bond0: + interfaces: [id0, id1] + parameters: + lacp-rate: fast +`[1:] + err := np.BridgeBondById("bond0", "br-bond0") + c.Assert(err, jc.ErrorIsNil) + + out, err := netplan.Marshal(np) + c.Assert(err, jc.ErrorIsNil) + c.Check(string(out), gc.Equals, expected) +} + +func (s *NetplanSuite) TestBridgerBondMissing(c *gc.C) { + np := MustNetplanFromYaml(c, ` +network: + version: 2 + renderer: NetworkManager + ethernets: + id0: + match: + macaddress: "00:11:22:33:44:55" + id0: + match: + macaddress: "00:11:22:33:44:66" + vlans: + id0.1234: + link: id0 + id: 1234 + bonds: + bond0: + interfaces: [id0, id1] + bridges: + not-juju-bridge: + interfaces: [bond0] + addresses: + - 1.2.3.4/24 + - 2000::1/64 + gateway4: 1.2.3.5 + gateway6: 2000::2 + nameservers: + search: [foo.local, bar.local] + addresses: [8.8.8.8] +`) + err := np.BridgeBondById("bond1", "br-bond1") + c.Check(err, gc.ErrorMatches, `bond device with id "bond1" for bridge "br-bond1" not found`) + c.Check(err, jc.Satisfies, errors.IsNotFound) +} + +func (s *NetplanSuite) TestFindEthernetByName(c *gc.C) { + np := MustNetplanFromYaml(c, ` network: version: 2 renderer: NetworkManager @@ -314,11 +944,10 @@ network: nameservers: search: [baz.local] addresses: [8.8.4.4] -`[1:] - var np netplan.Netplan - err := netplan.Unmarshal([]byte(input), &np) - c.Check(err, jc.ErrorIsNil) - + eno7: + addresses: + - 3.4.5.6/24 +`) device, err := np.FindEthernetByName("eno1") c.Assert(err, jc.ErrorIsNil) c.Check(device, gc.Equals, "id0") @@ -327,12 +956,17 @@ network: c.Assert(err, jc.ErrorIsNil) c.Check(device, gc.Equals, "id1") + device, err = np.FindEthernetByName("eno7") + c.Assert(err, jc.ErrorIsNil) + c.Check(device, gc.Equals, "eno7") + device, err = np.FindEthernetByName("eno5") c.Check(err, gc.ErrorMatches, "Ethernet device with name \"eno5\" not found") + c.Check(err, jc.Satisfies, errors.IsNotFound) } func (s *NetplanSuite) TestFindEthernetByMAC(c *gc.C) { - input := ` + np := MustNetplanFromYaml(c, ` network: version: 2 renderer: NetworkManager @@ -360,17 +994,268 @@ network: nameservers: search: [baz.local] addresses: [8.8.4.4] + id2: + addresses: + - 2.3.4.5/24 + macaddress: 00:11:22:33:44:77 +`) + device, err := np.FindEthernetByMAC("00:11:22:33:44:66") + c.Assert(err, jc.ErrorIsNil) + c.Check(device, gc.Equals, "id1") + + device, err = np.FindEthernetByMAC("00:11:22:33:44:88") + c.Check(err, gc.ErrorMatches, "Ethernet device with MAC \"00:11:22:33:44:88\" not found") + c.Check(err, jc.Satisfies, errors.IsNotFound) + + device, err = np.FindEthernetByMAC("00:11:22:33:44:77") + c.Assert(err, jc.ErrorIsNil) + c.Check(device, gc.Equals, "id2") +} + +func (s *NetplanSuite) TestFindVLANByName(c *gc.C) { + input := ` +network: + version: 2 + renderer: NetworkManager + ethernets: + id0: + match: + macaddress: "00:11:22:33:44:55" + addresses: + - 1.2.3.4/24 + - 2000::1/64 + gateway4: 1.2.3.5 + gateway6: 2000::2 + set-name: eno1 + nameservers: + search: [foo.local, bar.local] + addresses: [8.8.8.8] + vlans: + id0.123: + link: id0 + addresses: + - 2.3.4.5/24 `[1:] var np netplan.Netplan err := netplan.Unmarshal([]byte(input), &np) - c.Check(err, jc.ErrorIsNil) + c.Assert(err, jc.ErrorIsNil) - device, err := np.FindEthernetByMAC("00:11:22:33:44:66") + device, err := np.FindVLANByName("id0.123") c.Assert(err, jc.ErrorIsNil) - c.Check(device, gc.Equals, "id1") + c.Check(device, gc.Equals, "id0.123") - device, err = np.FindEthernetByMAC("00:11:22:33:44:88") - c.Check(err, gc.ErrorMatches, "Ethernet device with mac \"00:11:22:33:44:88\" not found") + device, err = np.FindVLANByName("id0") + c.Check(err, gc.ErrorMatches, "VLAN device with name \"id0\" not found") + c.Check(err, jc.Satisfies, errors.IsNotFound) +} + +func (s *NetplanSuite) TestFindVLANByMAC(c *gc.C) { + input := ` +network: + version: 2 + renderer: NetworkManager + ethernets: + id0: + match: + macaddress: "00:11:22:33:44:55" + addresses: + - 1.2.3.4/24 + - 2000::1/64 + gateway4: 1.2.3.5 + gateway6: 2000::2 + set-name: eno1 + nameservers: + search: [foo.local, bar.local] + addresses: [8.8.8.8] + vlans: + id0.123: + id: 123 + link: id0 + addresses: + - 2.3.4.5/24 + macaddress: 00:11:22:33:44:77 +`[1:] + var np netplan.Netplan + err := netplan.Unmarshal([]byte(input), &np) + c.Assert(err, jc.ErrorIsNil) + + device, err := np.FindVLANByMAC("00:11:22:33:44:77") + c.Assert(err, jc.ErrorIsNil) + c.Check(device, gc.Equals, "id0.123") + + // This is an Ethernet, not a VLAN + device, err = np.FindVLANByMAC("00:11:22:33:44:55") + c.Check(err, gc.ErrorMatches, `VLAN device with MAC "00:11:22:33:44:55" not found`) + c.Check(err, jc.Satisfies, errors.IsNotFound) +} + +func (s *NetplanSuite) TestFindBondByName(c *gc.C) { + input := ` +network: + version: 2 + renderer: NetworkManager + ethernets: + eno1: + match: + macaddress: "00:11:22:33:44:55" + set-name: eno1 + eno2: + match: + macaddress: "00:11:22:33:44:66" + set-name: eno2 + eno3: + match: + macaddress: "00:11:22:33:44:77" + set-name: eno3 + eno4: + match: + macaddress: "00:11:22:33:44:88" + set-name: eno4 + bonds: + bond0: + interfaces: [eno1, eno2] + bond1: + interfaces: [eno3, eno4] + macaddress: "00:11:22:33:44:77" + parameters: + primary: eno3 +`[1:] + var np netplan.Netplan + err := netplan.Unmarshal([]byte(input), &np) + c.Assert(err, jc.ErrorIsNil) + + device, err := np.FindBondByName("bond0") + c.Assert(err, jc.ErrorIsNil) + c.Check(device, gc.Equals, "bond0") + + device, err = np.FindBondByName("bond1") + c.Assert(err, jc.ErrorIsNil) + c.Check(device, gc.Equals, "bond1") + + device, err = np.FindBondByName("bond3") + c.Check(err, gc.ErrorMatches, "bond device with name \"bond3\" not found") + c.Check(err, jc.Satisfies, errors.IsNotFound) + + // eno4 is an Ethernet, not a Bond + device, err = np.FindBondByName("eno4") + c.Check(err, gc.ErrorMatches, "bond device with name \"eno4\" not found") + c.Check(err, jc.Satisfies, errors.IsNotFound) +} + +func (s *NetplanSuite) TestFindBondByMAC(c *gc.C) { + input := ` +network: + version: 2 + renderer: NetworkManager + ethernets: + eno1: + match: + macaddress: "00:11:22:33:44:55" + set-name: eno1 + eno2: + match: + macaddress: "00:11:22:33:44:66" + set-name: eno2 + eno3: + match: + macaddress: "00:11:22:33:44:77" + set-name: eno3 + eno4: + match: + macaddress: "00:11:22:33:44:88" + set-name: eno4 + bonds: + bond0: + interfaces: [eno1, eno2] + bond1: + interfaces: [eno3, eno4] + macaddress: "00:11:22:33:44:77" + parameters: + primary: eno3 +`[1:] + var np netplan.Netplan + err := netplan.Unmarshal([]byte(input), &np) + c.Assert(err, jc.ErrorIsNil) + + device, err := np.FindBondByMAC("00:11:22:33:44:77") + c.Assert(err, jc.ErrorIsNil) + c.Check(device, gc.Equals, "bond1") + + device, err = np.FindBondByMAC("00:11:22:33:44:99") + c.Check(err, gc.ErrorMatches, `bond device with MAC "00:11:22:33:44:99" not found`) + c.Check(err, jc.Satisfies, errors.IsNotFound) + + // This is an Ethernet, not a Bond + device, err = np.FindBondByMAC("00:11:22:33:44:55") + c.Check(err, gc.ErrorMatches, `bond device with MAC "00:11:22:33:44:55" not found`) + c.Check(err, jc.Satisfies, errors.IsNotFound) +} + +func checkFindDevice(c *gc.C, np *netplan.Netplan, name, mac, device string, dtype netplan.DeviceType, expErr string) { + foundDev, foundType, foundErr := np.FindDeviceByNameOrMAC(name, mac) + if expErr != "" { + c.Check(foundErr, gc.ErrorMatches, expErr) + c.Check(foundErr, jc.Satisfies, errors.IsNotFound) + } else { + c.Assert(foundErr, jc.ErrorIsNil) + c.Check(foundDev, gc.Equals, device) + c.Check(foundType, gc.Equals, dtype) + } +} + +func (s *NetplanSuite) TestFindDeviceByNameOrMAC(c *gc.C) { + np := MustNetplanFromYaml(c, ` +network: + version: 2 + renderer: NetworkManager + ethernets: + id0: + match: + macaddress: "00:11:22:33:44:55" + set-name: id0 + id1: + match: + macaddress: de:ad:be:ef:01:02 + set-name: id1 + eno3: + match: + macaddress: de:ad:be:ef:01:03 + set-name: eno3 + bonds: + bond0: + interfaces: [id0, id1] + parameters: + mode: 802.3ad + lacp-rate: fast + mii-monitor-interval: 100 + transmit-hash-policy: layer2 + up-delay: 0 + down-delay: 0 + vlans: + bond0.209: + id: 209 + link: bond0 + addresses: + - 123.123.123.123/24 + nameservers: + addresses: [8.8.8.8] + eno3.123: + id: 123 + link: eno3 + macaddress: de:ad:be:ef:01:03 +`) + checkFindDevice(c, np, "missing", "", "missing", "", + `device - name "missing" MAC "" not found`) + checkFindDevice(c, np, "missing", "dd:ee:ff:00:11:22", "missing", "", + `device - name "missing" MAC "dd:ee:ff:00:11:22" not found`) + checkFindDevice(c, np, "", "dd:ee:ff:00:11:22", "missing", "", + `device - name "" MAC "dd:ee:ff:00:11:22" not found`) + checkFindDevice(c, np, "eno3", "", "eno3", netplan.TypeEthernet, "") + checkFindDevice(c, np, "eno3", "de:ad:be:ef:01:03", "eno3", netplan.TypeEthernet, "") + checkFindDevice(c, np, "bond0", "", "bond0", netplan.TypeBond, "") + checkFindDevice(c, np, "bond0.209", "", "bond0.209", netplan.TypeVLAN, "") + checkFindDevice(c, np, "eno3.123", "de:ad:be:ef:01:03", "eno3.123", netplan.TypeVLAN, "") + checkFindDevice(c, np, "", "de:ad:be:ef:01:03", "eno3.123", netplan.TypeVLAN, "") } func (s *NetplanSuite) TestReadDirectory(c *gc.C) { @@ -416,7 +1301,7 @@ network: c.Assert(err, jc.ErrorIsNil) out, err := netplan.Marshal(np) - c.Check(err, jc.ErrorIsNil) + c.Assert(err, jc.ErrorIsNil) c.Check(string(out), gc.Equals, expected) } @@ -429,7 +1314,6 @@ network: renderer: NetworkManager ethernets: id0: - match: {} gateway4: 1.2.3.8 id1: match: @@ -456,7 +1340,7 @@ network: c.Assert(err, jc.ErrorIsNil) out, err := netplan.Marshal(np) - c.Check(err, jc.ErrorIsNil) + c.Assert(err, jc.ErrorIsNil) c.Check(string(out), gc.Equals, expected) } @@ -466,7 +1350,7 @@ network: version: 2 renderer: NetworkManager ethernets: - id0: + eno1: match: macaddress: "00:11:22:33:44:55" set-name: eno1 @@ -486,7 +1370,7 @@ network: driver: iwldvm bridges: juju-bridge: - interfaces: [id0] + interfaces: [eno1] addresses: - 1.2.3.4/24 - 2000::1/64 @@ -499,6 +1383,11 @@ network: interfaces: [id2] addresses: - 1.5.6.7/24 + vlans: + eno1.123: + id: 123 + link: eno1 + macaddress: "00:11:22:33:44:55" `[1:] tempDir := c.MkDir() files := []string{"00.yaml", "01.yaml"} @@ -513,17 +1402,17 @@ network: np, err := netplan.ReadDirectory(tempDir) c.Assert(err, jc.ErrorIsNil) - err = np.BridgeEthernetById("id0", "juju-bridge") - c.Check(err, jc.ErrorIsNil) + err = np.BridgeEthernetById("eno1", "juju-bridge") + c.Assert(err, jc.ErrorIsNil) generatedFile, err := np.Write("") - c.Check(err, jc.ErrorIsNil) + c.Assert(err, jc.ErrorIsNil) _, err = np.Write("") c.Check(err, gc.ErrorMatches, "Cannot write the same netplan twice") err = np.MoveYamlsToBak() - c.Check(err, jc.ErrorIsNil) + c.Assert(err, jc.ErrorIsNil) err = np.MoveYamlsToBak() c.Check(err, gc.ErrorMatches, "Cannot backup netplan yamls twice") @@ -548,7 +1437,7 @@ network: c.Check(string(data), gc.Equals, expected) err = np.Rollback() - c.Check(err, jc.ErrorIsNil) + c.Assert(err, jc.ErrorIsNil) fileInfos, err = ioutil.ReadDir(tempDir) c.Assert(err, jc.ErrorIsNil) @@ -570,16 +1459,18 @@ network: // We also check if writing to an explicit file works myPath := path.Join(tempDir, "my-own-path.yaml") outPath, err := np.Write(myPath) - c.Check(err, jc.ErrorIsNil) + c.Assert(err, jc.ErrorIsNil) c.Check(outPath, gc.Equals, myPath) data, err = ioutil.ReadFile(outPath) c.Check(string(data), gc.Equals, expected) err = np.MoveYamlsToBak() - c.Check(err, jc.ErrorIsNil) + c.Assert(err, jc.ErrorIsNil) } func (s *NetplanSuite) TestReadDirectoryMissing(c *gc.C) { + coretesting.SkipIfWindowsBug(c, "lp:1771077") + // On Windows the error is something like: "The system cannot find the file specified" tempDir := c.MkDir() os.RemoveAll(tempDir) _, err := netplan.ReadDirectory(tempDir) @@ -587,6 +1478,7 @@ func (s *NetplanSuite) TestReadDirectoryMissing(c *gc.C) { } func (s *NetplanSuite) TestReadDirectoryAccessDenied(c *gc.C) { + coretesting.SkipIfWindowsBug(c, "lp:1771077") tempDir := c.MkDir() err := ioutil.WriteFile(path.Join(tempDir, "00-file.yaml"), []byte("network:\n"), 00000) _, err = netplan.ReadDirectory(tempDir) @@ -601,6 +1493,7 @@ func (s *NetplanSuite) TestReadDirectoryBrokenYaml(c *gc.C) { } func (s *NetplanSuite) TestWritePermissionDenied(c *gc.C) { + coretesting.SkipIfWindowsBug(c, "lp:1771077") tempDir := c.MkDir() np, err := netplan.ReadDirectory(tempDir) c.Assert(err, jc.ErrorIsNil) @@ -630,7 +1523,6 @@ network: `[1:] var template = ` id%d: - match: {} set-name: foo.%d.%d `[1:] tempDir := c.MkDir() @@ -657,3 +1549,59 @@ network: } c.Check(string(writtenContent), gc.Equals, content) } + +type Example struct { + filename string + content string +} + +func readExampleStrings(c *gc.C) []Example { + fileInfos, err := ioutil.ReadDir("testdata/examples") + c.Assert(err, jc.ErrorIsNil) + var examples []Example + for _, finfo := range fileInfos { + if finfo.IsDir() { + continue + } + if strings.HasSuffix(finfo.Name(), ".yaml") { + f, err := os.Open("testdata/examples/" + finfo.Name()) + c.Assert(err, jc.ErrorIsNil) + content, err := ioutil.ReadAll(f) + f.Close() + c.Assert(err, jc.ErrorIsNil) + examples = append(examples, Example{ + filename: finfo.Name(), + content: string(content), + }) + } + } + // Make sure we find all the example files, if we change the count, update this number, but we don't allow the test + // suite to find the wrong number of files. + c.Assert(len(examples), gc.Equals, 13) + return examples +} + +func (s *NetplanSuite) TestNetplanExamples(c *gc.C) { + // these are the examples shipped by netplan, we should be able to read all of them + examples := readExampleStrings(c) + for _, example := range examples { + c.Logf("example: %s", example.filename) + var orig map[interface{}]interface{} + err := netplan.Unmarshal([]byte(example.content), &orig) + c.Assert(err, jc.ErrorIsNil, gc.Commentf("failed to unmarshal as map %s", example.filename)) + var np netplan.Netplan + err = netplan.Unmarshal([]byte(example.content), &np) + c.Check(err, jc.ErrorIsNil, gc.Commentf("failed to unmarshal %s", example.filename)) + // We don't assert that we exactly match the serialized form (we may output fields in a different order), + // but we do check that if we Marshal and then Unmarshal again, we get the same map contents. + // (We might also change boolean 'no' to 'false', etc. + out, err := netplan.Marshal(np) + c.Check(err, jc.ErrorIsNil, gc.Commentf("failed to marshal %s", example.filename)) + var roundtripped map[interface{}]interface{} + err = netplan.Unmarshal(out, &roundtripped) + if !reflect.DeepEqual(orig, roundtripped) { + pretty.Ldiff(c, orig, roundtripped) + c.Errorf("marshalling and unmarshalling %s did not contain the same content", example.filename) + } + } +} diff --git a/network/netplan/testdata/TestReadWriteBackup/00.yaml b/network/netplan/testdata/TestReadWriteBackup/00.yaml index 00bb768073a..37520dce24b 100644 --- a/network/netplan/testdata/TestReadWriteBackup/00.yaml +++ b/network/netplan/testdata/TestReadWriteBackup/00.yaml @@ -2,7 +2,7 @@ network: version: 2 renderer: NetworkManager ethernets: - id0: + eno1: match: macaddress: "00:11:22:33:44:55" addresses: @@ -25,3 +25,8 @@ network: nameservers: search: [baz.local] addresses: [8.8.4.4] + vlans: + eno1.123: + id: 123 + link: eno1 + macaddress: "00:11:22:33:44:55" diff --git a/network/netplan/testdata/examples/bonding.yaml b/network/netplan/testdata/examples/bonding.yaml new file mode 100644 index 00000000000..26adaf8d7dd --- /dev/null +++ b/network/netplan/testdata/examples/bonding.yaml @@ -0,0 +1,12 @@ +network: + version: 2 + renderer: networkd + bonds: + bond0: + dhcp4: yes + interfaces: + - enp3s0 + - enp4s0 + parameters: + mode: active-backup + primary: enp3s0 diff --git a/network/netplan/testdata/examples/bonding_router.yaml b/network/netplan/testdata/examples/bonding_router.yaml new file mode 100644 index 00000000000..c420e038483 --- /dev/null +++ b/network/netplan/testdata/examples/bonding_router.yaml @@ -0,0 +1,44 @@ +network: + version: 2 + renderer: networkd + ethernets: + enp1s0: + dhcp4: no + enp2s0: + dhcp4: no + enp3s0: + dhcp4: no + optional: true + enp4s0: + dhcp4: no + optional: true + enp5s0: + dhcp4: no + optional: true + enp6s0: + dhcp4: no + optional: true + bonds: + bond-lan: + interfaces: [enp2s0, enp3s0] + addresses: [192.168.93.2/24] + parameters: + mode: 802.3ad + mii-monitor-interval: 1 + bond-wan: + interfaces: [enp1s0, enp4s0] + addresses: [192.168.1.252/24] + gateway4: 192.168.1.1 + nameservers: + search: [local] + addresses: [8.8.8.8, 8.8.4.4] + parameters: + mode: active-backup + mii-monitor-interval: 1 + gratuitious-arp: 5 + bond-conntrack: + interfaces: [enp5s0, enp6s0] + addresses: [192.168.254.2/24] + parameters: + mode: balance-rr + mii-monitor-interval: 1 diff --git a/network/netplan/testdata/examples/bridge.yaml b/network/netplan/testdata/examples/bridge.yaml new file mode 100644 index 00000000000..89b9f00d1a3 --- /dev/null +++ b/network/netplan/testdata/examples/bridge.yaml @@ -0,0 +1,8 @@ +network: + version: 2 + renderer: networkd + bridges: + br0: + dhcp4: yes + interfaces: + - enp3s0 diff --git a/network/netplan/testdata/examples/bridge_vlan.yaml b/network/netplan/testdata/examples/bridge_vlan.yaml new file mode 100644 index 00000000000..b917b8486c3 --- /dev/null +++ b/network/netplan/testdata/examples/bridge_vlan.yaml @@ -0,0 +1,15 @@ +network: + version: 2 + renderer: networkd + ethernets: + enp0s25: + dhcp4: true + bridges: + br0: + addresses: [ 10.3.99.25/24 ] + interfaces: [ vlan15 ] + vlans: + vlan15: + accept-ra: no + id: 15 + link: enp0s25 diff --git a/network/netplan/testdata/examples/dhcp.yaml b/network/netplan/testdata/examples/dhcp.yaml new file mode 100644 index 00000000000..f7f85ef7053 --- /dev/null +++ b/network/netplan/testdata/examples/dhcp.yaml @@ -0,0 +1,6 @@ +network: + version: 2 + renderer: networkd + ethernets: + enp3s0: + dhcp4: true diff --git a/network/netplan/testdata/examples/loopback_interface.yaml b/network/netplan/testdata/examples/loopback_interface.yaml new file mode 100644 index 00000000000..734f0912da9 --- /dev/null +++ b/network/netplan/testdata/examples/loopback_interface.yaml @@ -0,0 +1,8 @@ +network: + version: 2 + renderer: networkd + ethernets: + lo: + match: + name: lo + addresses: [ 7.7.7.7/32 ] diff --git a/network/netplan/testdata/examples/network_manager.yaml b/network/netplan/testdata/examples/network_manager.yaml new file mode 100644 index 00000000000..b6547687957 --- /dev/null +++ b/network/netplan/testdata/examples/network_manager.yaml @@ -0,0 +1,3 @@ +network: + version: 2 + renderer: NetworkManager diff --git a/network/netplan/testdata/examples/source_routing.yaml b/network/netplan/testdata/examples/source_routing.yaml new file mode 100644 index 00000000000..1dba5f8ee34 --- /dev/null +++ b/network/netplan/testdata/examples/source_routing.yaml @@ -0,0 +1,27 @@ +network: + version: 2 + renderer: networkd + ethernets: + ens3: + addresses: + - 192.168.3.30/24 + dhcp4: no + routes: + - to: 192.168.3.0/24 + via: 192.168.3.1 + table: 101 + routing-policy: + - from: 192.168.3.0/24 + table: 101 + ens5: + addresses: + - 192.168.5.24/24 + dhcp4: no + gateway4: 192.168.5.1 + routes: + - to: 192.168.5.0/24 + via: 192.168.5.1 + table: 102 + routing-policy: + - from: 192.168.5.0/24 + table: 102 diff --git a/network/netplan/testdata/examples/static.yaml b/network/netplan/testdata/examples/static.yaml new file mode 100644 index 00000000000..e618909b0de --- /dev/null +++ b/network/netplan/testdata/examples/static.yaml @@ -0,0 +1,11 @@ +network: + version: 2 + renderer: networkd + ethernets: + enp3s0: + addresses: + - 10.10.10.2/24 + gateway4: 10.10.10.1 + nameservers: + search: [mydomain,otherdomain] + addresses: [10.10.10.1, 1.1.1.1] diff --git a/network/netplan/testdata/examples/static_multiaddress.yaml b/network/netplan/testdata/examples/static_multiaddress.yaml new file mode 100644 index 00000000000..90687e8c4d3 --- /dev/null +++ b/network/netplan/testdata/examples/static_multiaddress.yaml @@ -0,0 +1,9 @@ +network: + version: 2 + renderer: networkd + ethernets: + enp3s0: + addresses: + - 10.100.1.38/24 + - 10.100.1.39/24 + gateway4: 10.100.1.1 diff --git a/network/netplan/testdata/examples/vlan.yaml b/network/netplan/testdata/examples/vlan.yaml new file mode 100644 index 00000000000..4c506521c70 --- /dev/null +++ b/network/netplan/testdata/examples/vlan.yaml @@ -0,0 +1,25 @@ +network: + version: 2 + renderer: networkd + ethernets: + mainif: + match: + macaddress: "de:ad:be:ef:ca:fe" + set-name: mainif + addresses: [ "10.3.0.5/23" ] + gateway4: 10.3.0.1 + nameservers: + addresses: [ "8.8.8.8", "8.8.4.4" ] + search: [ example.com ] + vlans: + vlan15: + id: 15 + link: mainif + addresses: [ "10.3.99.5/24" ] + vlan10: + id: 10 + link: mainif + addresses: [ "10.3.98.5/24" ] + nameservers: + addresses: [ "127.0.0.1" ] + search: [ domain1.example.com, domain2.example.com ] diff --git a/network/netplan/testdata/examples/windows_dhcp_server.yaml b/network/netplan/testdata/examples/windows_dhcp_server.yaml new file mode 100644 index 00000000000..b4a178d8003 --- /dev/null +++ b/network/netplan/testdata/examples/windows_dhcp_server.yaml @@ -0,0 +1,6 @@ +network: + version: 2 + ethernets: + enp3s0: + dhcp4: yes + dhcp-identifier: mac diff --git a/network/netplan/testdata/examples/wireless.yaml b/network/netplan/testdata/examples/wireless.yaml new file mode 100644 index 00000000000..911382ec2bf --- /dev/null +++ b/network/netplan/testdata/examples/wireless.yaml @@ -0,0 +1,14 @@ +network: + version: 2 + renderer: networkd + wifis: + wlp2s0b1: + dhcp4: no + dhcp6: no + addresses: [192.168.0.21/24] + gateway4: 192.168.0.1 + nameservers: + addresses: [192.168.0.1, 8.8.8.8] + access-points: + "network_ssid_name": + password: "**********"