From 8b7b27339da2083624a0853c2de3f279191fea41 Mon Sep 17 00:00:00 2001 From: Marcus Weiner Date: Thu, 11 Aug 2022 15:13:20 +0200 Subject: [PATCH] Create first basic template (#6) Generate template for edgeos --- interop/edgeos/interfaces.go | 72 +++++++ templates/edgeos.go | 58 +++++ templates/edgeos.tpl | 39 ++++ templates/templates.go | 60 ++++++ templates/templates_test.go | 408 +++++++++++++++++++++++++++++++++++ templates/utils.go | 11 + 6 files changed, 648 insertions(+) create mode 100644 interop/edgeos/interfaces.go create mode 100644 templates/edgeos.go create mode 100644 templates/edgeos.tpl create mode 100644 templates/templates.go create mode 100644 templates/templates_test.go create mode 100644 templates/utils.go diff --git a/interop/edgeos/interfaces.go b/interop/edgeos/interfaces.go new file mode 100644 index 0000000..7e3ba6b --- /dev/null +++ b/interop/edgeos/interfaces.go @@ -0,0 +1,72 @@ +package edgeos + +import "github.com/ffddorf/confgen/netbox/models" + +type InterfaceType string + +const ( + InterfaceTypeBonding InterfaceType = "bonding" // Bonding interface name + InterfaceTypeBridge InterfaceType = "bridge" // Bridge interface name + InterfaceTypeEthernet InterfaceType = "ethernet" // Ethernet interface name + InterfaceTypeInput InterfaceType = "input" // Input functional block (IFB) interface name + InterfaceTypeIpv6Tunnel InterfaceType = "ipv6-tunnel" // IPv6 Tunnel interface + InterfaceTypeL2tpClient InterfaceType = "l2tp-client" // L2TP client interface name + InterfaceTypeL2tpv3 InterfaceType = "l2tpv3" // L2TPv3 interface + InterfaceTypeLoopback InterfaceType = "loopback" // Loopback interface name + InterfaceTypeOpenvpn InterfaceType = "openvpn" // OpenVPN tunnel interface name + InterfaceTypePptpClient InterfaceType = "pptp-client" // PPTP client interface name + InterfaceTypePseudoEthernet InterfaceType = "pseudo-ethernet" // Pseudo Ethernet device name + InterfaceTypeSwitch InterfaceType = "switch" // Switch interface name + InterfaceTypeTunnel InterfaceType = "tunnel" // Tunnel interface + InterfaceTypeVti InterfaceType = "vti" // Virtual Tunnel interface + InterfaceTypeWirelessmodem InterfaceType = "wirelessmodem" // Wireless modem interface name +) + +func InterfaceTypeFromNetbox(netboxType models.DcimInterfaceTypeChoices) (edgeosType InterfaceType, ok bool) { + ok = true + switch netboxType { + case models.DcimInterfaceTypeChoicesA100baseTx, + models.DcimInterfaceTypeChoicesA1000baseT, + models.DcimInterfaceTypeChoicesA25gbaseT, + models.DcimInterfaceTypeChoicesA5gbaseT, + models.DcimInterfaceTypeChoicesA10gbaseT, + models.DcimInterfaceTypeChoicesA10gbaseCx4, + models.DcimInterfaceTypeChoicesA1000baseXGbic, + models.DcimInterfaceTypeChoicesA1000baseXSfp, + models.DcimInterfaceTypeChoicesA10gbaseXSfpp, + models.DcimInterfaceTypeChoicesA10gbaseXXfp, + models.DcimInterfaceTypeChoicesA10gbaseXXenpak, + models.DcimInterfaceTypeChoicesA10gbaseXX2, + models.DcimInterfaceTypeChoicesA25gbaseXSfp28, + models.DcimInterfaceTypeChoicesA50gbaseXSfp56, + models.DcimInterfaceTypeChoicesA40gbaseXQsfpp, + models.DcimInterfaceTypeChoicesA50gbaseXSfp28, + models.DcimInterfaceTypeChoicesA100gbaseXCfp, + models.DcimInterfaceTypeChoicesA100gbaseXCfp2, + models.DcimInterfaceTypeChoicesA200gbaseXCfp2, + models.DcimInterfaceTypeChoicesA100gbaseXCfp4, + models.DcimInterfaceTypeChoicesA100gbaseXCpak, + models.DcimInterfaceTypeChoicesA100gbaseXQsfp28, + models.DcimInterfaceTypeChoicesA200gbaseXQsfp56, + models.DcimInterfaceTypeChoicesA400gbaseXQsfpdd, + models.DcimInterfaceTypeChoicesA400gbaseXOsfp, + models.DcimInterfaceTypeChoicesA1gfcSfp, + models.DcimInterfaceTypeChoicesA2gfcSfp, + models.DcimInterfaceTypeChoicesA4gfcSfp, + models.DcimInterfaceTypeChoicesA8gfcSfpp, + models.DcimInterfaceTypeChoicesA16gfcSfpp, + models.DcimInterfaceTypeChoicesA32gfcSfp28, + models.DcimInterfaceTypeChoicesA64gfcQsfpp, + models.DcimInterfaceTypeChoicesA128gfcQsfp28: + edgeosType = InterfaceTypeEthernet + case models.DcimInterfaceTypeChoicesBridge: + edgeosType = InterfaceTypeBridge + case models.DcimInterfaceTypeChoicesLag: + edgeosType = InterfaceTypeBonding + case models.DcimInterfaceTypeChoicesVirtual: + edgeosType = InterfaceTypeLoopback + default: + ok = false + } + return +} diff --git a/templates/edgeos.go b/templates/edgeos.go new file mode 100644 index 0000000..c54a0c6 --- /dev/null +++ b/templates/edgeos.go @@ -0,0 +1,58 @@ +package templates + +import ( + "strings" + + "github.com/ffddorf/confgen/interop/edgeos" + "github.com/ffddorf/confgen/netbox" + "github.com/ffddorf/confgen/netbox/models" +) + +func edgeosConfigFromMap(in map[string]interface{}) string { + out := new(strings.Builder) + if err := edgeos.ConfigFromMap(out, in, 0); err != nil { + panic(err) + } + return out.String() +} + +type ChildInterface = models.DeviceDeviceDeviceTypeInterfacesInterfaceTypeChild_interfacesInterfaceType + +type edgeosInterfaceDef struct { + netbox.Interface + VIFs []ChildInterface +} + +// Structures interfaces coming from netbox +// to be more compatible with the config +// structure of EdgeOS. +func edgeosPrepareInterfaces(interfaces []netbox.Interface) map[edgeos.InterfaceType][]edgeosInterfaceDef { + groups := make(map[edgeos.InterfaceType][]edgeosInterfaceDef) + for _, iface := range interfaces { + // find interface type, skip if not known + ifType, ok := edgeos.InterfaceTypeFromNetbox(iface.Type) + if !ok { + continue + } + + // skip interfaces that are children + if iface.Parent.Id != "" { + continue + } + + outIface := edgeosInterfaceDef{Interface: iface} + + // add child interfaces + if len(iface.Child_interfaces) > 0 { + outIface.VIFs = iface.Child_interfaces + } + + // add to list in map + if _, ok := groups[ifType]; !ok { + groups[ifType] = make([]edgeosInterfaceDef, 0, 1) + } + groups[ifType] = append(groups[ifType], outIface) + } + + return groups +} diff --git a/templates/edgeos.tpl b/templates/edgeos.tpl new file mode 100644 index 0000000..a69c7f0 --- /dev/null +++ b/templates/edgeos.tpl @@ -0,0 +1,39 @@ +interfaces { + {{- $ifGroups := edgeosPrepareInterfaces .Device.Interfaces }} + {{- range $ifType, $ifs := $ifGroups }} + {{- range $ifs }} + {{ $ifType }} {{ .Name }} { + {{- if .Description }} + description {{ .Description | maybeQuote }} + {{- end }} + {{- range .Ip_addresses }} + address {{ .Address }} + {{- end }} + {{- if not .Enabled }} + disable + {{- end }} + {{- if .Speed }} + speed {{ .Speed }} + {{- end }} + {{- if .Duplex }} + duplex {{ .Duplex }} + {{- end }} + {{- /* edgeosConfigFromMap .ConfigContext */}} + {{- range .VIFs }} + vif {{ .Untagged_vlan.Vid }} { + {{- if .Description }} + description {{ .Description | maybeQuote }} + {{- end }} + {{- range .Ip_addresses }} + address {{ .Address }} + {{- end }} + {{- if not .Enabled }} + disable + {{- end }} + {{- /* edgeosConfigFromMap .ConfigContext */}} + } + {{- end }} + } + {{- end }} + {{- end }} +} diff --git a/templates/templates.go b/templates/templates.go new file mode 100644 index 0000000..9eb6a2e --- /dev/null +++ b/templates/templates.go @@ -0,0 +1,60 @@ +package templates + +import ( + _ "embed" + "errors" + "io" + "text/template" + + "github.com/ffddorf/confgen/netbox" +) + +var ( + // all known templates by name + templates = map[string]*template.Template{} + + // common template functions + funcs = template.FuncMap{ + "maybeQuote": maybeQuote, + "edgeosConfigFromMap": edgeosConfigFromMap, + "edgeosPrepareInterfaces": edgeosPrepareInterfaces, + } +) + +// template bodies embedded via `go:embed` +var ( + //go:embed edgeos.tpl + edgeosRaw string + // ... load your template file here! +) + +func init() { + initTemplate("edgeos", edgeosRaw) + // ... initialize your template instance here! +} + +// used to initialize a template globally using its name and body +func initTemplate(name string, tpl string) { + templates[name] = template.Must( + template.New(name). + Funcs(funcs). + Parse(tpl), + ) +} + +// TemplateData is the data needed to execute a template +type TemplateData struct { + Device *netbox.Device +} + +var ErrTemplateNotFound = errors.New("template not found") + +// Render executes the template by the given name and +// writes the result into `out`. +func Render(out io.Writer, name string, data TemplateData) error { + templ, ok := templates[name] + if !ok { + return ErrTemplateNotFound + } + return templ.Execute(out, data) +} diff --git a/templates/templates_test.go b/templates/templates_test.go new file mode 100644 index 0000000..aeabae9 --- /dev/null +++ b/templates/templates_test.go @@ -0,0 +1,408 @@ +package templates_test + +import ( + "bytes" + "encoding/json" + "strings" + "testing" + + "github.com/ffddorf/confgen/netbox" + "github.com/ffddorf/confgen/templates" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEdgeOS(t *testing.T) { + device := &netbox.Device{} + require.NoError(t, json.Unmarshal([]byte(mockAPIData), &device)) + + data := templates.TemplateData{Device: device} + out := new(bytes.Buffer) + require.NoError(t, templates.Render(out, "edgeos", data)) + + assert.Equal(t, + strings.TrimSpace(out.String()), + strings.TrimSpace(expectedConfig), + ) +} + +const expectedConfig = ` +interfaces { + ethernet eth0 { + disable + } + ethernet eth1 { + disable + } + ethernet eth2 { + description "LAN management" + address 10.1.0.3/16 + address 2001:678:b7c:201:7a8a:20ff:fe46:cb0/64 + address fe80::7a8a:20ff:fe46:cb0/64 + vif 10 { + address 100.64.16.1/29 + address fdcb:aa6b:5532:25::1/64 + } + vif 11 { + address 10.129.0.76/31 + address fdcb:aa6b:5532:26::1/64 + } + } + ethernet eth3 { + disable + } + ethernet eth4 { + disable + } + ethernet eth5 { + disable + } + ethernet eth6 { + disable + } + ethernet eth7 { + description "CR4 interconnect" + address 45.151.166.10/31 + } + ethernet eth8 { + description "FFRL peering" + address 185.66.192.193/31 + address 2a03:2260:0:25::2/64 + } + loopback lo { + description Loopback + address 10.0.0.3/32 + address 45.151.166.3/32 + address 2001:678:b7c::3/128 + } +} +` + +const mockAPIData = ` +{ + "name": "CR3", + "rack": { + "name": "RK1" + }, + "location": { + "name": "Turm CoLo" + }, + "site": { + "name": "DUS2" + }, + "primary_ip4": { + "address": "45.151.166.3/32" + }, + "primary_ip6": { + "address": "2001:678:b7c::3/128" + }, + "interfaces": [ + { + "name": "eth0", + "description": "", + "type": "A_1000BASE_T", + "enabled": false, + "speed": null, + "duplex": null, + "ip_addresses": [], + "parent": null, + "child_interfaces": [] + }, + { + "name": "eth1", + "description": "", + "type": "A_1000BASE_X_SFP", + "enabled": false, + "speed": null, + "duplex": null, + "ip_addresses": [], + "parent": null, + "child_interfaces": [] + }, + { + "name": "eth2", + "description": "LAN management", + "type": "A_10GBASE_X_SFPP", + "enabled": true, + "speed": null, + "duplex": null, + "ip_addresses": [ + { + "address": "10.1.0.3/16" + }, + { + "address": "2001:678:b7c:201:7a8a:20ff:fe46:cb0/64" + }, + { + "address": "fe80::7a8a:20ff:fe46:cb0/64" + } + ], + "parent": null, + "child_interfaces": [ + { + "name": "eth2.10", + "description": "", + "type": "VIRTUAL", + "enabled": true, + "untagged_vlan": { + "vid": 10 + }, + "ip_addresses": [ + { + "address": "100.64.16.1/29" + }, + { + "address": "fdcb:aa6b:5532:25::1/64" + } + ] + }, + { + "name": "eth2.11", + "description": "", + "type": "VIRTUAL", + "enabled": true, + "untagged_vlan": { + "vid": 11 + }, + "ip_addresses": [ + { + "address": "10.129.0.76/31" + }, + { + "address": "fdcb:aa6b:5532:26::1/64" + } + ] + } + ] + }, + { + "name": "eth2.10", + "description": "", + "type": "VIRTUAL", + "enabled": true, + "speed": null, + "duplex": null, + "ip_addresses": [ + { + "address": "100.64.16.1/29" + }, + { + "address": "fdcb:aa6b:5532:25::1/64" + } + ], + "parent": { + "id": "91" + }, + "child_interfaces": [] + }, + { + "name": "eth2.11", + "description": "", + "type": "VIRTUAL", + "enabled": true, + "speed": null, + "duplex": null, + "ip_addresses": [ + { + "address": "10.129.0.76/31" + }, + { + "address": "fdcb:aa6b:5532:26::1/64" + } + ], + "parent": { + "id": "91" + }, + "child_interfaces": [] + }, + { + "name": "eth3", + "description": "", + "type": "A_10GBASE_X_SFPP", + "enabled": false, + "speed": null, + "duplex": null, + "ip_addresses": [], + "parent": null, + "child_interfaces": [] + }, + { + "name": "eth4", + "description": "", + "type": "A_10GBASE_X_SFPP", + "enabled": false, + "speed": null, + "duplex": null, + "ip_addresses": [], + "parent": null, + "child_interfaces": [] + }, + { + "name": "eth5", + "description": "", + "type": "A_10GBASE_X_SFPP", + "enabled": false, + "speed": null, + "duplex": null, + "ip_addresses": [], + "parent": null, + "child_interfaces": [] + }, + { + "name": "eth6", + "description": "", + "type": "A_10GBASE_X_SFPP", + "enabled": false, + "speed": null, + "duplex": null, + "ip_addresses": [], + "parent": null, + "child_interfaces": [] + }, + { + "name": "eth7", + "description": "CR4 interconnect", + "type": "A_10GBASE_X_SFPP", + "enabled": true, + "speed": null, + "duplex": null, + "ip_addresses": [ + { + "address": "45.151.166.10/31" + } + ], + "parent": null, + "child_interfaces": [] + }, + { + "name": "eth8", + "description": "FFRL peering", + "type": "A_10GBASE_X_SFPP", + "enabled": true, + "speed": null, + "duplex": null, + "ip_addresses": [ + { + "address": "185.66.192.193/31" + }, + { + "address": "2a03:2260:0:25::2/64" + } + ], + "parent": null, + "child_interfaces": [] + }, + { + "name": "lo", + "description": "Loopback", + "type": "VIRTUAL", + "enabled": true, + "speed": null, + "duplex": null, + "ip_addresses": [ + { + "address": "10.0.0.3/32" + }, + { + "address": "45.151.166.3/32" + }, + { + "address": "2001:678:b7c::3/128" + } + ], + "parent": null, + "child_interfaces": [] + }, + { + "name": "tun0", + "description": "R12 dago", + "type": "OTHER", + "enabled": true, + "speed": null, + "duplex": null, + "ip_addresses": [ + { + "address": "10.129.0.0/31" + }, + { + "address": "fdcb:aa6b:5532:1::1/64" + } + ], + "parent": null, + "child_interfaces": [] + }, + { + "name": "tun1", + "description": "R9 voelklinger", + "type": "OTHER", + "enabled": true, + "speed": null, + "duplex": null, + "ip_addresses": [ + { + "address": "10.129.0.2/31" + }, + { + "address": "fdcb:aa6b:5532:2::1/64" + } + ], + "parent": null, + "child_interfaces": [] + }, + { + "name": "tun2", + "description": "R10 niessdonk", + "type": "OTHER", + "enabled": true, + "speed": null, + "duplex": null, + "ip_addresses": [ + { + "address": "10.129.0.4/31" + }, + { + "address": "fdcb:aa6b:5532:3::1/64" + } + ], + "parent": null, + "child_interfaces": [] + }, + { + "name": "tun3", + "description": "R15 Gatherweg", + "type": "OTHER", + "enabled": true, + "speed": null, + "duplex": null, + "ip_addresses": [ + { + "address": "10.129.0.10/31" + }, + { + "address": "fdcb:aa6b:5532:6::1/64" + } + ], + "parent": null, + "child_interfaces": [] + }, + { + "name": "tun4", + "description": "R14 hoeherweg", + "type": "OTHER", + "enabled": true, + "speed": null, + "duplex": null, + "ip_addresses": [ + { + "address": "10.129.0.14/31" + }, + { + "address": "fdcb:aa6b:5532:8::1/64" + } + ], + "parent": null, + "child_interfaces": [] + } + ] +} +` diff --git a/templates/utils.go b/templates/utils.go new file mode 100644 index 0000000..cfa098d --- /dev/null +++ b/templates/utils.go @@ -0,0 +1,11 @@ +package templates + +import "strings" + +// quotes a string if it contains whitespace +func maybeQuote(in string) string { + if strings.ContainsAny(in, " \n\t") { + return `"` + in + `"` + } + return in +}