summaryrefslogtreecommitdiffhomepage
path: root/dhcpv6/ztpv6
diff options
context:
space:
mode:
Diffstat (limited to 'dhcpv6/ztpv6')
-rw-r--r--dhcpv6/ztpv6/README.md16
-rw-r--r--dhcpv6/ztpv6/parse_remote_id.go84
-rw-r--r--dhcpv6/ztpv6/parse_remote_id_test.go88
-rw-r--r--dhcpv6/ztpv6/parse_vendor_options.go60
-rw-r--r--dhcpv6/ztpv6/parse_vendor_options_test.go55
5 files changed, 303 insertions, 0 deletions
diff --git a/dhcpv6/ztpv6/README.md b/dhcpv6/ztpv6/README.md
new file mode 100644
index 0000000..f44191b
--- /dev/null
+++ b/dhcpv6/ztpv6/README.md
@@ -0,0 +1,16 @@
+# Zero Touch Provisioning (ZTP) DHCPv6 Parsing for Network Hardware Vendors
+
+## Currently Supported Vendors For DHCPv6 ZTP
+ - Arista
+ - ZPE
+
+## Why Do We Need This?
+Many network hardware vendors support features that allow network devices to provision themselves with proper supporting automation/tools. Network devices can rely on DHCP and other methods to gather bootfile info, IPs, etc. DHCPv6 Vendor options provides us Vendor Name, Make, Model, and Serial Number data. This data can be used to uniquely identify individual network devices at provisioning time and can be used by tooling to make decisions necessary to correctly and reliably provision a network device.
+
+For more details on a large-scale ZTP deployment, check out how this is done at Facebook, [Scaling Backbone Networks Through Zero Touch Provisioning](https://code.fb.com/networking-traffic/scaling-the-facebook-backbone-through-zero-touch-provisioning/).
+
+
+### Example Data
+Vendor specific data is commonly in a delimiter separated format containing Vendor Name, Model, Make, and Serial Number. This of course will vary per vendor and there could be more or less data.
+Vendor;Model;Version;SerialNumber
+`Arista;DCS-7060;01.011;ZZZ00000000`
diff --git a/dhcpv6/ztpv6/parse_remote_id.go b/dhcpv6/ztpv6/parse_remote_id.go
new file mode 100644
index 0000000..5991e96
--- /dev/null
+++ b/dhcpv6/ztpv6/parse_remote_id.go
@@ -0,0 +1,84 @@
+package ztpv6
+
+import (
+ "errors"
+ "fmt"
+ "regexp"
+
+ "github.com/insomniacslk/dhcp/dhcpv6"
+)
+
+var (
+ // Arista Port, Vlan Pattern
+ aristaPVPattern = regexp.MustCompile("Ethernet(?P<port>[0-9]+):(?P<vlan>[0-9]+)")
+ // Arista Slot, Mod, Port Pattern
+ aristaSMPPattern = regexp.MustCompile("Ethernet(?P<slot>[0-9]+)/(?P<module>[0-9]+)/(?P<port>[0-9]+)")
+)
+
+// CircuitID represents the structure of network vendor interface formats
+type CircuitID struct {
+ Slot string
+ Module string
+ Port string
+ SubPort string
+ Vlan string
+}
+
+// ParseRemoteId will parse the RemoteId Option data for Vendor Specific data
+func ParseRemoteId(packet dhcpv6.DHCPv6) (*CircuitID, error) {
+ // Need to decapsulate the packet after multiple relays in order to reach RemoteId data
+ inner, err := dhcpv6.DecapsulateRelayIndex(packet, -1)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decapsulate relay index: %v", err)
+ }
+
+ if rid := inner.GetOneOption(dhcpv6.OptionRemoteID); rid != nil {
+ remoteID := string(rid.(*dhcpv6.OptRemoteId).RemoteID())
+ circ, err := matchCircuitId(remoteID)
+ if err != nil {
+ return nil, err
+ }
+ return circ, nil
+ }
+ return nil, errors.New("failed to parse RemoteID option data")
+}
+
+func matchCircuitId(remoteID string) (*CircuitID, error) {
+ var names, matches []string
+
+ switch {
+ case aristaPVPattern.MatchString(remoteID):
+ matches = aristaPVPattern.FindStringSubmatch(remoteID)
+ names = aristaPVPattern.SubexpNames()
+ case aristaSMPPattern.MatchString(remoteID):
+ matches = aristaSMPPattern.FindStringSubmatch(remoteID)
+ names = aristaSMPPattern.SubexpNames()
+ }
+
+ if len(matches) == 0 {
+ return nil, fmt.Errorf("no circuitId regex matches for %v", remoteID)
+ }
+
+ var circuit CircuitID
+ for i, match := range matches {
+ switch names[i] {
+ case "port":
+ circuit.Port = match
+ case "slot":
+ circuit.Slot = match
+ case "module":
+ circuit.Module = match
+ case "subport":
+ circuit.SubPort = match
+ case "vlan":
+ circuit.Vlan = match
+ }
+ }
+
+ return &circuit, nil
+}
+
+// FormatCircuitID is the CircuitID format we send in our Bootfile URL for ZTP devices
+func (c *CircuitID) FormatCircuitID() string {
+ return fmt.Sprintf("%v,%v,%v,%v,%v", c.Slot, c.Module, c.Port, c.SubPort, c.Vlan)
+}
diff --git a/dhcpv6/ztpv6/parse_remote_id_test.go b/dhcpv6/ztpv6/parse_remote_id_test.go
new file mode 100644
index 0000000..54fccc9
--- /dev/null
+++ b/dhcpv6/ztpv6/parse_remote_id_test.go
@@ -0,0 +1,88 @@
+package ztpv6
+
+import (
+ "testing"
+
+ "github.com/insomniacslk/dhcp/dhcpv6"
+ "github.com/stretchr/testify/require"
+)
+
+func TestCircuitID(t *testing.T) {
+ tt := []struct {
+ name string
+ circuit string
+ want *CircuitID
+ fail bool
+ }{
+ {name: "Bogus string", circuit: "ope/1/2/3:ope", fail: true, want: nil},
+ {name: "Arista Port Vlan Pattern", circuit: "Ethernet13:2001", want: &CircuitID{Port: "13", Vlan: "2001"}},
+ {name: "Arista Slot Module Port Pattern", circuit: "Ethernet1/3/4", want: &CircuitID{Slot: "1", Module: "3", Port: "4"}},
+ }
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ circuit, err := matchCircuitId(tc.circuit)
+ if err != nil && !tc.fail {
+ t.Errorf("unexpected failure: %v", err)
+ }
+ if circuit != nil {
+ require.Equal(t, *tc.want, *circuit, "comparing remoteID data")
+ }
+ })
+ }
+}
+
+func TestFormatCircuitID(t *testing.T) {
+ tt := []struct {
+ name string
+ circuit *CircuitID
+ want string
+ fail bool
+ }{
+ {name: "empty", circuit: &CircuitID{}, want: ",,,,"},
+ {name: "Arista format Port/Vlan", circuit: &CircuitID{Port: "13", Vlan: "2001"}, want: ",,13,,2001"},
+ {name: "Arista format Slot/Module/Port", circuit: &CircuitID{Slot: "1", Module: "3", Port: "4"}, want: "1,3,4,,"},
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ circuit := tc.circuit.FormatCircuitID()
+ require.Equal(t, tc.want, circuit, "FormatRemoteID data")
+ })
+ }
+
+}
+
+func TestParseRemoteID(t *testing.T) {
+ tt := []struct {
+ name string
+ circuit []byte
+ want *CircuitID
+ fail bool
+ }{
+ {name: "Bogus string", circuit: []byte("ope/1/2/3:ope.1"), fail: true, want: nil},
+ {name: "Arista Port Vlan Pattern", circuit: []byte("Ethernet13:2001"), want: &CircuitID{Port: "13", Vlan: "2001"}},
+ {name: "Arista Slot Module Port Pattern", circuit: []byte("Ethernet1/3/4"), want: &CircuitID{Slot: "1", Module: "3", Port: "4"}},
+ }
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ packet, err := dhcpv6.NewMessage()
+ if err != nil {
+ t.Fatalf("failed to creat dhcpv6 packet object: %v", err)
+ }
+ opt := dhcpv6.OptRemoteId{}
+ opt.SetRemoteID(tc.circuit)
+ opt.SetEnterpriseNumber(1234)
+ packet.AddOption(&opt)
+
+ circuit, err := ParseRemoteId(packet)
+ if err != nil && !tc.fail {
+ t.Errorf("unexpected failure: %v", err)
+ }
+ if circuit != nil {
+ require.Equal(t, *tc.want, *circuit, "ZTPRemoteID data")
+ } else {
+ require.Equal(t, tc.want, circuit, "ZTPRemoteID data")
+ }
+ })
+ }
+}
diff --git a/dhcpv6/ztpv6/parse_vendor_options.go b/dhcpv6/ztpv6/parse_vendor_options.go
new file mode 100644
index 0000000..63a6a05
--- /dev/null
+++ b/dhcpv6/ztpv6/parse_vendor_options.go
@@ -0,0 +1,60 @@
+package ztpv6
+
+import (
+ "errors"
+ "strings"
+
+ "github.com/insomniacslk/dhcp/dhcpv6"
+)
+
+var (
+ errVendorOptionMalformed = errors.New("malformed vendor option")
+)
+
+// VendorData contains fields extracted from Option 17 data
+type VendorData struct {
+ VendorName, Model, Serial string
+}
+
+// ParseVendorData will try to parse dhcp6 Vendor Specific Information options data
+// looking for more specific vendor data (like model, serial number, etc).
+// If the options are missing we will just return nil
+func ParseVendorData(packet dhcpv6.DHCPv6) (*VendorData, error) {
+ opt := packet.GetOneOption(dhcpv6.OptionVendorOpts)
+ if opt == nil {
+ return nil, errors.New("vendor options not found")
+ }
+
+ vd := VendorData{}
+ vo := opt.(*dhcpv6.OptVendorOpts).VendorOpts
+
+ for _, opt := range vo {
+ optData := string(opt.(*dhcpv6.OptionGeneric).OptionData)
+ switch {
+ // Arista;DCS-0000;00.00;ZZZ00000000
+ case strings.HasPrefix(optData, "Arista;"):
+ p := strings.Split(optData, ";")
+ if len(p) < 4 {
+ return nil, errVendorOptionMalformed
+ }
+
+ vd.VendorName = p[0]
+ vd.Model = p[1]
+ vd.Serial = p[3]
+ return &vd, nil
+
+ // ZPESystems:NSC:000000000
+ case strings.HasPrefix(optData, "ZPESystems:"):
+ p := strings.Split(optData, ":")
+ if len(p) < 3 {
+ return nil, errVendorOptionMalformed
+ }
+
+ vd.VendorName = p[0]
+ vd.Model = p[1]
+ vd.Serial = p[2]
+ return &vd, nil
+ }
+ }
+ return nil, errors.New("failed to parse vendor option data")
+}
diff --git a/dhcpv6/ztpv6/parse_vendor_options_test.go b/dhcpv6/ztpv6/parse_vendor_options_test.go
new file mode 100644
index 0000000..a494fac
--- /dev/null
+++ b/dhcpv6/ztpv6/parse_vendor_options_test.go
@@ -0,0 +1,55 @@
+package ztpv6
+
+import (
+ "testing"
+
+ "github.com/insomniacslk/dhcp/dhcpv6"
+ "github.com/stretchr/testify/require"
+)
+
+func TestParseVendorData(t *testing.T) {
+ tt := []struct {
+ name string
+ vc, hostname string
+ want *VendorData
+ fail bool
+ }{
+ {name: "empty", fail: true},
+ {name: "unknownVendor", vc: "VendorX;BFR10K;XX12345", fail: true, want: nil},
+ {name: "truncatedArista", vc: "Arista;1234", fail: true, want: nil},
+ {name: "truncatedZPE", vc: "ZPESystems:1234", fail: true, want: nil},
+ {
+ name: "arista",
+ vc: "Arista;DCS-7050S-64;01.23;JPE12345678",
+ want: &VendorData{VendorName: "Arista", Model: "DCS-7050S-64", Serial: "JPE12345678"},
+ }, {
+ name: "zpe",
+ vc: "ZPESystems:NSC:001234567",
+ want: &VendorData{VendorName: "ZPESystems", Model: "NSC", Serial: "001234567"},
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.name, func(t *testing.T) {
+ packet, err := dhcpv6.NewMessage()
+ if err != nil {
+ t.Fatalf("failed to creat dhcpv6 packet object: %v", err)
+ }
+
+ opts := []dhcpv6.Option{&dhcpv6.OptionGeneric{OptionData: []byte(tc.vc), OptionCode: 1}}
+ packet.AddOption(&dhcpv6.OptVendorOpts{
+ VendorOpts: opts, EnterpriseNumber: 0000})
+
+ vd, err := ParseVendorData(packet)
+ if err != nil && !tc.fail {
+ t.Errorf("unexpected failure: %v", err)
+ }
+
+ if vd != nil {
+ require.Equal(t, *tc.want, *vd, "comparing vendor option data")
+ } else {
+ require.Equal(t, tc.want, vd, "comparing vendor option data")
+ }
+ })
+ }
+}