diff --git a/Documentation/debian-interfaces.md b/Documentation/debian-interfaces.md new file mode 100644 index 0000000..d3c64a1 --- /dev/null +++ b/Documentation/debian-interfaces.md @@ -0,0 +1,27 @@ +#Debian Interfaces# +**WARNING**: This option is EXPERIMENTAL and may change or be removed at any +point. +There is basic support for converting from a Debian network configuration to +networkd unit files. The -convert-netconf=debian option is used to activate +this feature. + +#convert-netconf# +Default: "" +Read the network config provided in cloud-drive and translate it from the +specified format into networkd unit files (requires the -from-configdrive +flag). Currently only supports "debian" which provides support for a small +subset of the [Debian network configuration] +(https://wiki.debian.org/NetworkConfiguration). These options include: + +- interface config methods + - static + - address/netmask + - gateway + - hwaddress + - dns-nameservers + - dhcp + - hwaddress + - manual + - loopback +- vlan_raw_device +- bond-slaves diff --git a/network/interface.go b/network/interface.go index a139a67..be0b04b 100644 --- a/network/interface.go +++ b/network/interface.go @@ -7,15 +7,23 @@ import ( type InterfaceGenerator interface { Name() string + Filename() string Netdev() string Link() string Network() string } +type networkInterface interface { + InterfaceGenerator + Children() []networkInterface + setConfigDepth(int) +} + type logicalInterface struct { - name string - config configMethod - children []InterfaceGenerator + name string + config configMethod + children []networkInterface + configDepth int } func (i *logicalInterface) Network() string { @@ -48,6 +56,22 @@ func (i *logicalInterface) Network() string { return config } +func (i *logicalInterface) Link() string { + return "" +} + +func (i *logicalInterface) Filename() string { + return fmt.Sprintf("%02x-%s", i.configDepth, i.name) +} + +func (i *logicalInterface) Children() []networkInterface { + return i.children +} + +func (i *logicalInterface) setConfigDepth(depth int) { + i.configDepth = depth +} + type physicalInterface struct { logicalInterface } @@ -60,10 +84,6 @@ func (p *physicalInterface) Netdev() string { return "" } -func (p *physicalInterface) Link() string { - return "" -} - type bondInterface struct { logicalInterface slaves []string @@ -77,10 +97,6 @@ func (b *bondInterface) Netdev() string { return fmt.Sprintf("[NetDev]\nKind=bond\nName=%s\n", b.name) } -func (b *bondInterface) Link() string { - return "" -} - type vlanInterface struct { logicalInterface id int @@ -92,102 +108,146 @@ func (v *vlanInterface) Name() string { } func (v *vlanInterface) Netdev() string { - return fmt.Sprintf("[NetDev]\nKind=vlan\nName=%s\n\n[VLAN]\nId=%d\n", v.name, v.id) -} - -func (v *vlanInterface) Link() string { - return "" + config := fmt.Sprintf("[NetDev]\nKind=vlan\nName=%s\n", v.name) + switch c := v.config.(type) { + case configMethodStatic: + if c.hwaddress != nil { + config += fmt.Sprintf("MACAddress=%s\n", c.hwaddress) + } + case configMethodDHCP: + if c.hwaddress != nil { + config += fmt.Sprintf("MACAddress=%s\n", c.hwaddress) + } + } + config += fmt.Sprintf("\n[VLAN]\nId=%d\n", v.id) + return config } func buildInterfaces(stanzas []*stanzaInterface) []InterfaceGenerator { - bondStanzas := make(map[string]*stanzaInterface) - physicalStanzas := make(map[string]*stanzaInterface) - vlanStanzas := make(map[string]*stanzaInterface) - for _, iface := range stanzas { - switch iface.kind { - case interfaceBond: - bondStanzas[iface.name] = iface - case interfacePhysical: - physicalStanzas[iface.name] = iface - case interfaceVLAN: - vlanStanzas[iface.name] = iface - } - } + interfaceMap := createInterfaces(stanzas) + linkAncestors(interfaceMap) + markConfigDepths(interfaceMap) - physicals := make(map[string]*physicalInterface) - for _, p := range physicalStanzas { - if _, ok := p.configMethod.(configMethodLoopback); ok { - continue - } - physicals[p.name] = &physicalInterface{ - logicalInterface{ - name: p.name, - config: p.configMethod, - children: []InterfaceGenerator{}, - }, - } - } - - bonds := make(map[string]*bondInterface) - for _, b := range bondStanzas { - bonds[b.name] = &bondInterface{ - logicalInterface{ - name: b.name, - config: b.configMethod, - children: []InterfaceGenerator{}, - }, - b.options["slaves"], - } - } - - vlans := make(map[string]*vlanInterface) - for _, v := range vlanStanzas { - var rawDevice string - id, _ := strconv.Atoi(v.options["id"][0]) - if device := v.options["raw_device"]; len(device) == 1 { - rawDevice = device[0] - } - vlans[v.name] = &vlanInterface{ - logicalInterface{ - name: v.name, - config: v.configMethod, - children: []InterfaceGenerator{}, - }, - id, - rawDevice, - } - } - - for _, vlan := range vlans { - if physical, ok := physicals[vlan.rawDevice]; ok { - physical.children = append(physical.children, vlan) - } - if bond, ok := bonds[vlan.rawDevice]; ok { - bond.children = append(bond.children, vlan) - } - } - - for _, bond := range bonds { - for _, slave := range bond.slaves { - if physical, ok := physicals[slave]; ok { - physical.children = append(physical.children, bond) - } - if pBond, ok := bonds[slave]; ok { - pBond.children = append(pBond.children, bond) - } - } - } - - interfaces := make([]InterfaceGenerator, 0, len(physicals)+len(bonds)+len(vlans)) - for _, physical := range physicals { - interfaces = append(interfaces, physical) - } - for _, bond := range bonds { - interfaces = append(interfaces, bond) - } - for _, vlan := range vlans { - interfaces = append(interfaces, vlan) + interfaces := make([]InterfaceGenerator, 0, len(interfaceMap)) + for _, iface := range interfaceMap { + interfaces = append(interfaces, iface) } return interfaces } + +func createInterfaces(stanzas []*stanzaInterface) map[string]networkInterface { + interfaceMap := make(map[string]networkInterface) + for _, iface := range stanzas { + switch iface.kind { + case interfaceBond: + interfaceMap[iface.name] = &bondInterface{ + logicalInterface{ + name: iface.name, + config: iface.configMethod, + children: []networkInterface{}, + }, + iface.options["slaves"], + } + for _, slave := range iface.options["slaves"] { + if _, ok := interfaceMap[slave]; !ok { + interfaceMap[slave] = &physicalInterface{ + logicalInterface{ + name: slave, + config: configMethodManual{}, + children: []networkInterface{}, + }, + } + } + } + + case interfacePhysical: + if _, ok := iface.configMethod.(configMethodLoopback); ok { + continue + } + interfaceMap[iface.name] = &physicalInterface{ + logicalInterface{ + name: iface.name, + config: iface.configMethod, + children: []networkInterface{}, + }, + } + + case interfaceVLAN: + var rawDevice string + id, _ := strconv.Atoi(iface.options["id"][0]) + if device := iface.options["raw_device"]; len(device) == 1 { + rawDevice = device[0] + if _, ok := interfaceMap[rawDevice]; !ok { + interfaceMap[rawDevice] = &physicalInterface{ + logicalInterface{ + name: rawDevice, + config: configMethodManual{}, + children: []networkInterface{}, + }, + } + } + } + interfaceMap[iface.name] = &vlanInterface{ + logicalInterface{ + name: iface.name, + config: iface.configMethod, + children: []networkInterface{}, + }, + id, + rawDevice, + } + } + } + return interfaceMap +} + +func linkAncestors(interfaceMap map[string]networkInterface) { + for _, iface := range interfaceMap { + switch i := iface.(type) { + case *vlanInterface: + if parent, ok := interfaceMap[i.rawDevice]; ok { + switch p := parent.(type) { + case *physicalInterface: + p.children = append(p.children, iface) + case *bondInterface: + p.children = append(p.children, iface) + } + } + case *bondInterface: + for _, slave := range i.slaves { + if parent, ok := interfaceMap[slave]; ok { + switch p := parent.(type) { + case *physicalInterface: + p.children = append(p.children, iface) + case *bondInterface: + p.children = append(p.children, iface) + } + } + } + } + } +} + +func markConfigDepths(interfaceMap map[string]networkInterface) { + rootInterfaceMap := make(map[string]networkInterface) + for k, v := range interfaceMap { + rootInterfaceMap[k] = v + } + + for _, iface := range interfaceMap { + for _, child := range iface.Children() { + delete(rootInterfaceMap, child.Name()) + } + } + for _, iface := range rootInterfaceMap { + setDepth(iface, 0) + } +} + +func setDepth(iface networkInterface, depth int) { + iface.setConfigDepth(depth) + for _, child := range iface.Children() { + setDepth(child, depth+1) + } +} diff --git a/network/interface_test.go b/network/interface_test.go index 7ebe80e..c98bf60 100644 --- a/network/interface_test.go +++ b/network/interface_test.go @@ -30,7 +30,7 @@ func TestPhysicalInterfaceLink(t *testing.T) { func TestPhysicalInterfaceNetwork(t *testing.T) { p := physicalInterface{logicalInterface{ name: "testname", - children: []InterfaceGenerator{ + children: []networkInterface{ &bondInterface{ logicalInterface{ name: "testbond1", @@ -96,7 +96,7 @@ func TestBondInterfaceNetwork(t *testing.T) { logicalInterface{ name: "testname", config: configMethodDHCP{}, - children: []InterfaceGenerator{ + children: []networkInterface{ &bondInterface{ logicalInterface{ name: "testbond1", @@ -143,16 +143,26 @@ func TestVLANInterfaceName(t *testing.T) { } func TestVLANInterfaceNetdev(t *testing.T) { - v := vlanInterface{logicalInterface{name: "testname"}, 1, ""} - netdev := `[NetDev] -Kind=vlan -Name=testname - -[VLAN] -Id=1 -` - if v.Netdev() != netdev { - t.FailNow() + for _, tt := range []struct { + i vlanInterface + l string + }{ + { + vlanInterface{logicalInterface{name: "testname"}, 1, ""}, + "[NetDev]\nKind=vlan\nName=testname\n\n[VLAN]\nId=1\n", + }, + { + vlanInterface{logicalInterface{name: "testname", config: configMethodStatic{hwaddress: net.HardwareAddr([]byte{0, 1, 2, 3, 4, 5})}}, 1, ""}, + "[NetDev]\nKind=vlan\nName=testname\nMACAddress=00:01:02:03:04:05\n\n[VLAN]\nId=1\n", + }, + { + vlanInterface{logicalInterface{name: "testname", config: configMethodDHCP{hwaddress: net.HardwareAddr([]byte{0, 1, 2, 3, 4, 5})}}, 1, ""}, + "[NetDev]\nKind=vlan\nName=testname\nMACAddress=00:01:02:03:04:05\n\n[VLAN]\nId=1\n", + }, + } { + if tt.i.Netdev() != tt.l { + t.Fatalf("bad netdev config (%q): got %q, want %q", tt.i, tt.i.Netdev(), tt.l) + } } } @@ -224,6 +234,80 @@ func TestBuildInterfacesLo(t *testing.T) { } } +func TestBuildInterfacesBlindBond(t *testing.T) { + stanzas := []*stanzaInterface{ + { + name: "bond0", + kind: interfaceBond, + auto: false, + configMethod: configMethodManual{}, + options: map[string][]string{ + "slaves": []string{"eth0"}, + }, + }, + } + interfaces := buildInterfaces(stanzas) + bond0 := &bondInterface{ + logicalInterface{ + name: "bond0", + config: configMethodManual{}, + children: []networkInterface{}, + configDepth: 1, + }, + []string{"eth0"}, + } + eth0 := &physicalInterface{ + logicalInterface{ + name: "eth0", + config: configMethodManual{}, + children: []networkInterface{bond0}, + configDepth: 0, + }, + } + expect := []InterfaceGenerator{bond0, eth0} + if !reflect.DeepEqual(interfaces, expect) { + t.FailNow() + } +} + +func TestBuildInterfacesBlindVLAN(t *testing.T) { + stanzas := []*stanzaInterface{ + { + name: "vlan0", + kind: interfaceVLAN, + auto: false, + configMethod: configMethodManual{}, + options: map[string][]string{ + "id": []string{"0"}, + "raw_device": []string{"eth0"}, + }, + }, + } + interfaces := buildInterfaces(stanzas) + vlan0 := &vlanInterface{ + logicalInterface{ + name: "vlan0", + config: configMethodManual{}, + children: []networkInterface{}, + configDepth: 1, + }, + 0, + "eth0", + } + eth0 := &physicalInterface{ + logicalInterface{ + name: "eth0", + config: configMethodManual{}, + children: []networkInterface{vlan0}, + configDepth: 0, + }, + } + expect := []InterfaceGenerator{eth0, vlan0} + if !reflect.DeepEqual(interfaces, expect) { + t.FailNow() + } +} + func TestBuildInterfaces(t *testing.T) { stanzas := []*stanzaInterface{ &stanzaInterface{ @@ -275,43 +359,48 @@ func TestBuildInterfaces(t *testing.T) { interfaces := buildInterfaces(stanzas) vlan1 := &vlanInterface{ logicalInterface{ - name: "vlan1", - config: configMethodManual{}, - children: []InterfaceGenerator{}, + name: "vlan1", + config: configMethodManual{}, + children: []networkInterface{}, + configDepth: 2, }, 1, "bond0", } vlan0 := &vlanInterface{ logicalInterface{ - name: "vlan0", - config: configMethodManual{}, - children: []InterfaceGenerator{}, + name: "vlan0", + config: configMethodManual{}, + children: []networkInterface{}, + configDepth: 1, }, 0, "eth0", } bond1 := &bondInterface{ logicalInterface{ - name: "bond1", - config: configMethodManual{}, - children: []InterfaceGenerator{}, + name: "bond1", + config: configMethodManual{}, + children: []networkInterface{}, + configDepth: 2, }, []string{"bond0"}, } bond0 := &bondInterface{ logicalInterface{ - name: "bond0", - config: configMethodManual{}, - children: []InterfaceGenerator{vlan1, bond1}, + name: "bond0", + config: configMethodManual{}, + children: []networkInterface{bond1, vlan1}, + configDepth: 1, }, []string{"eth0"}, } eth0 := &physicalInterface{ logicalInterface{ - name: "eth0", - config: configMethodManual{}, - children: []InterfaceGenerator{vlan0, bond0}, + name: "eth0", + config: configMethodManual{}, + children: []networkInterface{bond0, vlan0}, + configDepth: 0, }, } expect := []InterfaceGenerator{eth0, bond0, bond1, vlan0, vlan1} @@ -319,3 +408,19 @@ func TestBuildInterfaces(t *testing.T) { t.FailNow() } } + +func TestFilename(t *testing.T) { + for _, tt := range []struct { + i logicalInterface + f string + }{ + {logicalInterface{name: "iface", configDepth: 0}, "00-iface"}, + {logicalInterface{name: "iface", configDepth: 9}, "09-iface"}, + {logicalInterface{name: "iface", configDepth: 10}, "0a-iface"}, + {logicalInterface{name: "iface", configDepth: 53}, "35-iface"}, + } { + if tt.i.Filename() != tt.f { + t.Fatalf("bad filename (%q): got %q, want %q", tt.i, tt.i.Filename(), tt.f) + } + } +} diff --git a/network/stanza.go b/network/stanza.go index dca59aa..005231b 100644 --- a/network/stanza.go +++ b/network/stanza.go @@ -40,13 +40,16 @@ type configMethodStatic struct { address net.IPNet nameservers []net.IP routes []route + hwaddress net.HardwareAddr } type configMethodLoopback struct{} type configMethodManual struct{} -type configMethodDHCP struct{} +type configMethodDHCP struct { + hwaddress net.HardwareAddr +} func parseStanzas(lines []string) (stanzas []stanza, err error) { rawStanzas, err := splitStanzas(lines) @@ -96,7 +99,7 @@ func splitStanzas(lines []string) ([][]string, error) { } else if curStanza != nil { curStanza = append(curStanza, line) } else { - return nil, fmt.Errorf("missing stanza start '%s'", line) + return nil, fmt.Errorf("missing stanza start %q", line) } } @@ -142,7 +145,7 @@ func parseStanza(rawStanza []string) (stanza, error) { case "iface": return parseInterfaceStanza(attributes, rawStanza[1:]) default: - return nil, fmt.Errorf("unknown stanza '%s'", kind) + return nil, fmt.Errorf("unknown stanza %q", kind) } } @@ -204,7 +207,7 @@ func parseInterfaceStanza(attributes []string, options []string) (*stanzaInterfa } } if config.address.IP == nil || config.address.Mask == nil { - return nil, fmt.Errorf("malformed static network config for '%s'", iface) + return nil, fmt.Errorf("malformed static network config for %q", iface) } if gateways, ok := optionMap["gateway"]; ok { if len(gateways) == 1 { @@ -217,6 +220,11 @@ func parseInterfaceStanza(attributes []string, options []string) (*stanzaInterfa }) } } + if hwaddress, err := parseHwaddress(optionMap, iface); err == nil { + config.hwaddress = hwaddress + } else { + return nil, err + } for _, nameserver := range optionMap["dns-nameservers"] { config.nameservers = append(config.nameservers, net.ParseIP(nameserver)) } @@ -245,9 +253,15 @@ func parseInterfaceStanza(attributes []string, options []string) (*stanzaInterfa case "manual": conf = configMethodManual{} case "dhcp": - conf = configMethodDHCP{} + config := configMethodDHCP{} + if hwaddress, err := parseHwaddress(optionMap, iface); err == nil { + config.hwaddress = hwaddress + } else { + return nil, err + } + conf = config default: - return nil, fmt.Errorf("invalid config method '%s'", confMethod) + return nil, fmt.Errorf("invalid config method %q", confMethod) } if _, ok := optionMap["vlan_raw_device"]; ok { @@ -265,6 +279,19 @@ func parseInterfaceStanza(attributes []string, options []string) (*stanzaInterfa return parsePhysicalStanza(iface, conf, attributes, optionMap) } +func parseHwaddress(options map[string][]string, iface string) (net.HardwareAddr, error) { + if hwaddress, ok := options["hwaddress"]; ok && len(hwaddress) == 2 { + switch hwaddress[0] { + case "ether": + if address, err := net.ParseMAC(hwaddress[1]); err == nil { + return address, nil + } + return nil, fmt.Errorf("malformed hwaddress option for %q", iface) + } + } + return nil, nil +} + func parseBondStanza(iface string, conf configMethod, attributes []string, options map[string][]string) (*stanzaInterface, error) { options["slaves"] = options["bond-slaves"] return &stanzaInterface{name: iface, kind: interfaceBond, configMethod: conf, options: options}, nil @@ -282,11 +309,11 @@ func parseVLANStanza(iface string, conf configMethod, attributes []string, optio } else if strings.HasPrefix(iface, "vlan") { id = strings.TrimPrefix(iface, "vlan") } else { - return nil, fmt.Errorf("malformed vlan name %s", iface) + return nil, fmt.Errorf("malformed vlan name %q", iface) } if _, err := strconv.Atoi(id); err != nil { - return nil, fmt.Errorf("malformed vlan name %s", iface) + return nil, fmt.Errorf("malformed vlan name %q", iface) } options["id"] = []string{id} options["raw_device"] = options["vlan_raw_device"] diff --git a/network/stanza_test.go b/network/stanza_test.go index bddd292..440dcf8 100644 --- a/network/stanza_test.go +++ b/network/stanza_test.go @@ -42,6 +42,8 @@ func TestBadParseInterfaceStanza(t *testing.T) { {[]string{"eth", "inet", "static"}, []string{"netmask 255.255.255.0"}, "malformed static network config"}, {[]string{"eth", "inet", "static"}, []string{"address invalid", "netmask 255.255.255.0"}, "malformed static network config"}, {[]string{"eth", "inet", "static"}, []string{"address 192.168.1.100", "netmask invalid"}, "malformed static network config"}, + {[]string{"eth", "inet", "static"}, []string{"address 192.168.1.100", "netmask 255.255.255.0", "hwaddress ether NotAnAddress"}, "malformed hwaddress option"}, + {[]string{"eth", "inet", "dhcp"}, []string{"hwaddress ether NotAnAddress"}, "malformed hwaddress option"}, } { _, err := parseInterfaceStanza(tt.in, tt.opts) if err == nil || !strings.HasPrefix(err.Error(), tt.e) { @@ -407,7 +409,46 @@ func TestParseInterfaceStanzaOptions(t *testing.T) { } } -func TestParseInterfaceStazaBond(t *testing.T) { +func TestParseInterfaceStanzaHwaddress(t *testing.T) { + for _, tt := range []struct { + attr []string + opt []string + hw net.HardwareAddr + }{ + { + []string{"mybond", "inet", "dhcp"}, + []string{}, + nil, + }, + { + []string{"mybond", "inet", "dhcp"}, + []string{"hwaddress ether 00:01:02:03:04:05"}, + net.HardwareAddr([]byte{0, 1, 2, 3, 4, 5}), + }, + { + []string{"mybond", "inet", "static"}, + []string{"hwaddress ether 00:01:02:03:04:05", "address 192.168.1.100", "netmask 255.255.255.0"}, + net.HardwareAddr([]byte{0, 1, 2, 3, 4, 5}), + }, + } { + iface, err := parseInterfaceStanza(tt.attr, tt.opt) + if err != nil { + t.Fatalf("error in parseInterfaceStanza (%q, %q): %q", tt.attr, tt.opt, err) + } + switch c := iface.configMethod.(type) { + case configMethodStatic: + if !reflect.DeepEqual(c.hwaddress, tt.hw) { + t.Fatalf("bad hwaddress (%q, %q): got %q, want %q", tt.attr, tt.opt, c.hwaddress, tt.hw) + } + case configMethodDHCP: + if !reflect.DeepEqual(c.hwaddress, tt.hw) { + t.Fatalf("bad hwaddress (%q, %q): got %q, want %q", tt.attr, tt.opt, c.hwaddress, tt.hw) + } + } + } +} + +func TestParseInterfaceStanzaBond(t *testing.T) { iface, err := parseInterfaceStanza([]string{"mybond", "inet", "manual"}, []string{"bond-slaves eth"}) if err != nil { t.FailNow() @@ -417,7 +458,7 @@ func TestParseInterfaceStazaBond(t *testing.T) { } } -func TestParseInterfaceStazaVLANName(t *testing.T) { +func TestParseInterfaceStanzaVLANName(t *testing.T) { iface, err := parseInterfaceStanza([]string{"eth0.1", "inet", "manual"}, nil) if err != nil { t.FailNow() @@ -427,7 +468,7 @@ func TestParseInterfaceStazaVLANName(t *testing.T) { } } -func TestParseInterfaceStazaVLANOption(t *testing.T) { +func TestParseInterfaceStanzaVLANOption(t *testing.T) { iface, err := parseInterfaceStanza([]string{"vlan1", "inet", "manual"}, []string{"vlan_raw_device eth"}) if err != nil { t.FailNow() diff --git a/system/networkd.go b/system/networkd.go index c795563..a982869 100644 --- a/system/networkd.go +++ b/system/networkd.go @@ -36,7 +36,9 @@ func downNetworkInterfaces(interfaces []network.InterfaceGenerator) error { sysInterfaceMap := make(map[string]*net.Interface) if systemInterfaces, err := net.Interfaces(); err == nil { for _, iface := range systemInterfaces { - sysInterfaceMap[iface.Name] = &iface + // Need a copy of the interface so we can take the address + temp := iface + sysInterfaceMap[temp.Name] = &temp } } else { return err @@ -64,15 +66,15 @@ func restartNetworkd() error { func WriteNetworkdConfigs(interfaces []network.InterfaceGenerator) error { for _, iface := range interfaces { - filename := path.Join(runtimeNetworkPath, fmt.Sprintf("%s.netdev", iface.Name())) + filename := path.Join(runtimeNetworkPath, fmt.Sprintf("%s.netdev", iface.Filename())) if err := writeConfig(filename, iface.Netdev()); err != nil { return err } - filename = path.Join(runtimeNetworkPath, fmt.Sprintf("%s.link", iface.Name())) + filename = path.Join(runtimeNetworkPath, fmt.Sprintf("%s.link", iface.Filename())) if err := writeConfig(filename, iface.Link()); err != nil { return err } - filename = path.Join(runtimeNetworkPath, fmt.Sprintf("%s.network", iface.Name())) + filename = path.Join(runtimeNetworkPath, fmt.Sprintf("%s.network", iface.Filename())) if err := writeConfig(filename, iface.Network()); err != nil { return err }