From 38321fedce7cbc5cd710fc6d459ed4101fe22b1c Mon Sep 17 00:00:00 2001 From: Alex Crawford Date: Thu, 22 May 2014 13:42:09 -0700 Subject: [PATCH] feat(interfaces): Add support for interfaces file This adds the ability for cloudinit to parse a debian interfaces file and generate the coresponding networkd configs. --- network/interface.go | 193 ++++++++++++++++++++++++++++ network/network.go | 86 +++++++++++++ network/stanza.go | 295 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 574 insertions(+) create mode 100644 network/interface.go create mode 100644 network/network.go create mode 100644 network/stanza.go diff --git a/network/interface.go b/network/interface.go new file mode 100644 index 0000000..a139a67 --- /dev/null +++ b/network/interface.go @@ -0,0 +1,193 @@ +package network + +import ( + "fmt" + "strconv" +) + +type InterfaceGenerator interface { + Name() string + Netdev() string + Link() string + Network() string +} + +type logicalInterface struct { + name string + config configMethod + children []InterfaceGenerator +} + +func (i *logicalInterface) Network() string { + config := fmt.Sprintf("[Match]\nName=%s\n\n[Network]\n", i.name) + + for _, child := range i.children { + switch iface := child.(type) { + case *vlanInterface: + config += fmt.Sprintf("VLAN=%s\n", iface.name) + case *bondInterface: + config += fmt.Sprintf("Bond=%s\n", iface.name) + } + } + + switch conf := i.config.(type) { + case configMethodStatic: + for _, nameserver := range conf.nameservers { + config += fmt.Sprintf("DNS=%s\n", nameserver) + } + if conf.address.IP != nil { + config += fmt.Sprintf("\n[Address]\nAddress=%s\n", conf.address.String()) + } + for _, route := range conf.routes { + config += fmt.Sprintf("\n[Route]\nDestination=%s\nGateway=%s\n", route.destination.String(), route.gateway) + } + case configMethodDHCP: + config += "DHCP=true\n" + } + + return config +} + +type physicalInterface struct { + logicalInterface +} + +func (p *physicalInterface) Name() string { + return p.name +} + +func (p *physicalInterface) Netdev() string { + return "" +} + +func (p *physicalInterface) Link() string { + return "" +} + +type bondInterface struct { + logicalInterface + slaves []string +} + +func (b *bondInterface) Name() string { + return b.name +} + +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 + rawDevice string +} + +func (v *vlanInterface) Name() string { + return v.name +} + +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 "" +} + +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 + } + } + + 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) + } + + return interfaces +} diff --git a/network/network.go b/network/network.go new file mode 100644 index 0000000..2f23098 --- /dev/null +++ b/network/network.go @@ -0,0 +1,86 @@ +package network + +import ( + "fmt" + "io" + "os" + "path" + "strings" +) + +func ProcessDebianNetconf(config string) ([]InterfaceGenerator, error) { + lines := formatConfig(config) + stanzas, err := parseStanzas(lines) + if err != nil { + return nil, err + } + + interfaces := make([]*stanzaInterface, 0, len(stanzas)) + for _, stanza := range stanzas { + switch s := stanza.(type) { + case *stanzaInterface: + interfaces = append(interfaces, s) + } + } + + return buildInterfaces(interfaces), nil +} + +func WriteConfigs(configPath string, interfaces []InterfaceGenerator) error { + if err := os.MkdirAll(configPath, os.ModePerm+os.ModeDir); err != nil { + fmt.Println(err) + os.Exit(1) + } + + for _, iface := range interfaces { + filename := path.Join(configPath, fmt.Sprintf("%s.netdev", iface.Name())) + if err := writeConfig(filename, iface.GenerateNetdevConfig()); err != nil { + return err + } + filename = path.Join(configPath, fmt.Sprintf("%s.link", iface.Name())) + if err := writeConfig(filename, iface.GenerateLinkConfig()); err != nil { + return err + } + filename = path.Join(configPath, fmt.Sprintf("%s.network", iface.Name())) + if err := writeConfig(filename, iface.GenerateNetworkConfig()); err != nil { + return err + } + } + return nil +} + +func writeConfig(filename string, config string) error { + if config == "" { + return nil + } + + if file, err := os.Create(filename); err == nil { + io.WriteString(file, config) + file.Close() + return nil + } else { + return err + } +} + +func formatConfig(config string) []string { + lines := []string{} + config = strings.Replace(config, "\\\n", "", -1) + for config != "" { + split := strings.SplitN(config, "\n", 2) + line := strings.TrimSpace(split[0]) + + if len(split) == 2 { + config = split[1] + } else { + config = "" + } + + if strings.HasPrefix(line, "#") || line == "" { + continue + } + + lines = append(lines, line) + } + return lines +} diff --git a/network/stanza.go b/network/stanza.go new file mode 100644 index 0000000..dca59aa --- /dev/null +++ b/network/stanza.go @@ -0,0 +1,295 @@ +package network + +import ( + "fmt" + "net" + "strconv" + "strings" +) + +type stanza interface{} + +type stanzaAuto struct { + interfaces []string +} + +type stanzaInterface struct { + name string + kind interfaceKind + auto bool + configMethod configMethod + options map[string][]string +} + +type interfaceKind int + +const ( + interfaceBond = interfaceKind(iota) + interfacePhysical + interfaceVLAN +) + +type route struct { + destination net.IPNet + gateway net.IP +} + +type configMethod interface{} + +type configMethodStatic struct { + address net.IPNet + nameservers []net.IP + routes []route +} + +type configMethodLoopback struct{} + +type configMethodManual struct{} + +type configMethodDHCP struct{} + +func parseStanzas(lines []string) (stanzas []stanza, err error) { + rawStanzas, err := splitStanzas(lines) + if err != nil { + return nil, err + } + + stanzas = make([]stanza, 0, len(rawStanzas)) + for _, rawStanza := range rawStanzas { + if stanza, err := parseStanza(rawStanza); err == nil { + stanzas = append(stanzas, stanza) + } else { + return nil, err + } + } + + autos := make([]string, 0) + interfaceMap := make(map[string]*stanzaInterface) + for _, stanza := range stanzas { + switch c := stanza.(type) { + case *stanzaAuto: + autos = append(autos, c.interfaces...) + case *stanzaInterface: + interfaceMap[c.name] = c + } + } + + // Apply the auto attribute + for _, auto := range autos { + if iface, ok := interfaceMap[auto]; ok { + iface.auto = true + } + } + + return stanzas, nil +} + +func splitStanzas(lines []string) ([][]string, error) { + var curStanza []string + stanzas := make([][]string, 0) + for _, line := range lines { + if isStanzaStart(line) { + if curStanza != nil { + stanzas = append(stanzas, curStanza) + } + curStanza = []string{line} + } else if curStanza != nil { + curStanza = append(curStanza, line) + } else { + return nil, fmt.Errorf("missing stanza start '%s'", line) + } + } + + if curStanza != nil { + stanzas = append(stanzas, curStanza) + } + + return stanzas, nil +} + +func isStanzaStart(line string) bool { + switch strings.Split(line, " ")[0] { + case "auto": + fallthrough + case "iface": + fallthrough + case "mapping": + return true + } + + if strings.HasPrefix(line, "allow-") { + return true + } + + return false +} + +func parseStanza(rawStanza []string) (stanza, error) { + if len(rawStanza) == 0 { + panic("empty stanza") + } + tokens := strings.Fields(rawStanza[0]) + if len(tokens) < 2 { + return nil, fmt.Errorf("malformed stanza start %q", rawStanza[0]) + } + + kind := tokens[0] + attributes := tokens[1:] + + switch kind { + case "auto": + return parseAutoStanza(attributes, rawStanza[1:]) + case "iface": + return parseInterfaceStanza(attributes, rawStanza[1:]) + default: + return nil, fmt.Errorf("unknown stanza '%s'", kind) + } +} + +func parseAutoStanza(attributes []string, options []string) (*stanzaAuto, error) { + return &stanzaAuto{interfaces: attributes}, nil +} + +func parseInterfaceStanza(attributes []string, options []string) (*stanzaInterface, error) { + if len(attributes) != 3 { + return nil, fmt.Errorf("incorrect number of attributes") + } + + iface := attributes[0] + confMethod := attributes[2] + + optionMap := make(map[string][]string, 0) + for _, option := range options { + if strings.HasPrefix(option, "post-up") { + tokens := strings.SplitAfterN(option, " ", 2) + if len(tokens) != 2 { + continue + } + if v, ok := optionMap["post-up"]; ok { + optionMap["post-up"] = append(v, tokens[1]) + } else { + optionMap["post-up"] = []string{tokens[1]} + } + } else if strings.HasPrefix(option, "pre-down") { + tokens := strings.SplitAfterN(option, " ", 2) + if len(tokens) != 2 { + continue + } + if v, ok := optionMap["pre-down"]; ok { + optionMap["pre-down"] = append(v, tokens[1]) + } else { + optionMap["pre-down"] = []string{tokens[1]} + } + } else { + tokens := strings.Fields(option) + optionMap[tokens[0]] = tokens[1:] + } + } + + var conf configMethod + switch confMethod { + case "static": + config := configMethodStatic{ + routes: make([]route, 0), + nameservers: make([]net.IP, 0), + } + if addresses, ok := optionMap["address"]; ok { + if len(addresses) == 1 { + config.address.IP = net.ParseIP(addresses[0]) + } + } + if netmasks, ok := optionMap["netmask"]; ok { + if len(netmasks) == 1 { + config.address.Mask = net.IPMask(net.ParseIP(netmasks[0]).To4()) + } + } + if config.address.IP == nil || config.address.Mask == nil { + return nil, fmt.Errorf("malformed static network config for '%s'", iface) + } + if gateways, ok := optionMap["gateway"]; ok { + if len(gateways) == 1 { + config.routes = append(config.routes, route{ + destination: net.IPNet{ + IP: net.IPv4(0, 0, 0, 0), + Mask: net.IPv4Mask(0, 0, 0, 0), + }, + gateway: net.ParseIP(gateways[0]), + }) + } + } + for _, nameserver := range optionMap["dns-nameservers"] { + config.nameservers = append(config.nameservers, net.ParseIP(nameserver)) + } + for _, postup := range optionMap["post-up"] { + if strings.HasPrefix(postup, "route add") { + route := route{} + fields := strings.Fields(postup) + for i, field := range fields[:len(fields)-1] { + switch field { + case "-net": + route.destination.IP = net.ParseIP(fields[i+1]) + case "netmask": + route.destination.Mask = net.IPMask(net.ParseIP(fields[i+1]).To4()) + case "gw": + route.gateway = net.ParseIP(fields[i+1]) + } + } + if route.destination.IP != nil && route.destination.Mask != nil && route.gateway != nil { + config.routes = append(config.routes, route) + } + } + } + conf = config + case "loopback": + conf = configMethodLoopback{} + case "manual": + conf = configMethodManual{} + case "dhcp": + conf = configMethodDHCP{} + default: + return nil, fmt.Errorf("invalid config method '%s'", confMethod) + } + + if _, ok := optionMap["vlan_raw_device"]; ok { + return parseVLANStanza(iface, conf, attributes, optionMap) + } + + if strings.Contains(iface, ".") { + return parseVLANStanza(iface, conf, attributes, optionMap) + } + + if _, ok := optionMap["bond-slaves"]; ok { + return parseBondStanza(iface, conf, attributes, optionMap) + } + + return parsePhysicalStanza(iface, conf, attributes, optionMap) +} + +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 +} + +func parsePhysicalStanza(iface string, conf configMethod, attributes []string, options map[string][]string) (*stanzaInterface, error) { + return &stanzaInterface{name: iface, kind: interfacePhysical, configMethod: conf, options: options}, nil +} + +func parseVLANStanza(iface string, conf configMethod, attributes []string, options map[string][]string) (*stanzaInterface, error) { + var id string + if strings.Contains(iface, ".") { + tokens := strings.Split(iface, ".") + id = tokens[len(tokens)-1] + } else if strings.HasPrefix(iface, "vlan") { + id = strings.TrimPrefix(iface, "vlan") + } else { + return nil, fmt.Errorf("malformed vlan name %s", iface) + } + + if _, err := strconv.Atoi(id); err != nil { + return nil, fmt.Errorf("malformed vlan name %s", iface) + } + options["id"] = []string{id} + options["raw_device"] = options["vlan_raw_device"] + + return &stanzaInterface{name: iface, kind: interfaceVLAN, configMethod: conf, options: options}, nil +}