diff options
author | Akshay Navale <akna8887@colorado.edu> | 2019-03-13 16:56:17 -0600 |
---|---|---|
committer | Pablo Mazzini <pmazzini@gmail.com> | 2019-03-13 22:56:17 +0000 |
commit | 47951531f8c905cc79209028dd08a8894763043e (patch) | |
tree | a4e9fe1ba9fe7fd136b27d2cb669cef8c710f8e8 /dhcpv4 | |
parent | 1c542dbd7c4522d965f58dfff8090184d38fca2d (diff) |
Adding CircuitId parsing logic into dhcpv4 lib (#255)
Diffstat (limited to 'dhcpv4')
-rw-r--r-- | dhcpv4/option_relay_agent_information.go | 20 | ||||
-rw-r--r-- | dhcpv4/ztpv4/parse_circuitid.go | 94 | ||||
-rw-r--r-- | dhcpv4/ztpv4/parse_circuitid_test.go | 109 |
3 files changed, 223 insertions, 0 deletions
diff --git a/dhcpv4/option_relay_agent_information.go b/dhcpv4/option_relay_agent_information.go index 8b88b0c..56d93f2 100644 --- a/dhcpv4/option_relay_agent_information.go +++ b/dhcpv4/option_relay_agent_information.go @@ -36,3 +36,23 @@ func (r *RelayOptions) FromBytes(data []byte) error { func OptRelayAgentInfo(o ...Option) Option { return Option{Code: OptionRelayAgentInformation, Value: RelayOptions{OptionsFromList(o...)}} } + +type raiSubOptionCode uint8 + +func (o raiSubOptionCode) Code() uint8 { + return uint8(o) +} + +func (o raiSubOptionCode) String() string { + if s, ok := raiSubOptionCodeToString[o]; ok { + return s + } + return fmt.Sprintf("unknown (%d)", o) +} + +// AgentCircuitIDSubOption as per https://tools.ietf.org/html/rfc3046#section-2.0 +const AgentCircuitIDSubOption raiSubOptionCode = 1 + +var raiSubOptionCodeToString = map[raiSubOptionCode]string{ + AgentCircuitIDSubOption: "Agent Circuit ID Sub-option", +} diff --git a/dhcpv4/ztpv4/parse_circuitid.go b/dhcpv4/ztpv4/parse_circuitid.go new file mode 100644 index 0000000..b9db358 --- /dev/null +++ b/dhcpv4/ztpv4/parse_circuitid.go @@ -0,0 +1,94 @@ +package ztpv4 + +import ( + "fmt" + "regexp" + + "github.com/insomniacslk/dhcp/dhcpv4" +) + +// CircuitID represents the structure of network vendor interface formats +type CircuitID struct { + Slot string + Module string + Port string + SubPort string + Vlan string +} + +var circuitRegexs = []*regexp.Regexp{ + // Juniper QFX et-0/0/0:0.0 + regexp.MustCompile("^et-(?P<slot>[0-9]+)/(?P<mod>[0-9]+)/(?P<port>[0-9]+):(?P<subport>[0-9]+).*$"), + // Juniper PTX et-0/0/0.0 + regexp.MustCompile("^et-(?P<slot>[0-9]+)/(?P<mod>[0-9]+)/(?P<port>[0-9]+).(?P<subport>[0-9]+)$"), + // Arista Ethernet3/17/1 + regexp.MustCompile("^Ethernet(?P<slot>[0-9]+)/(?P<mod>[0-9]+)/(?P<port>[0-9]+)$"), + // Juniper QFX et-1/0/61 + regexp.MustCompile("^et-(?P<slot>[0-9]+)/(?P<mod>[0-9]+)/(?P<port>[0-9]+)$"), + // Arista Ethernet14:Vlan2001 + // Arista Ethernet10:2020 + regexp.MustCompile("^Ethernet(?P<port>[0-9]+):(?P<vlan>.*)$"), + // Cisco Gi1/10:2020 + regexp.MustCompile("^Gi(?P<slot>[0-9]+)/(?P<port>[0-9]+):(?P<vlan>.*)$"), + // Nexus Ethernet1/3 + regexp.MustCompile("^Ethernet(?P<slot>[0-9]+)/(?P<port>[0-9]+)$"), + // Juniper bundle interface ae52.0 + regexp.MustCompile("^ae(?P<port>[0-9]+).(?P<subport>[0-9])$"), +} + +// ParseCircuitID will parse dhcpv4 packet and return CircuitID info +func ParseCircuitID(packet *dhcpv4.DHCPv4) (*CircuitID, error) { + // CircuitId info is stored as sub-option field in RelayAgentInformationOption + relayOptions := packet.RelayAgentInfo() + + if relayOptions == nil { + return nil, fmt.Errorf("No relay agent information option found in the dhcpv4 pkt") + } + + // As per RFC 3046 sub-Option 1 is circuit-id. Look at 2.0 section in that RFC + // https://tools.ietf.org/html/rfc3046 + circuitIdStr := string(relayOptions.Options.Get(dhcpv4.AgentCircuitIDSubOption)) + if circuitIdStr == "" { + return nil, fmt.Errorf("no circuit-id suboption found in dhcpv4 packet") + } + circuitId, err := matchCircuitID(circuitIdStr) + if err != nil { + return nil, err + } + return circuitId, nil +} + +func matchCircuitID(circuitIdStr string) (*CircuitID, error) { + + for _, re := range circuitRegexs { + + match := re.FindStringSubmatch(circuitIdStr) + if len(match) == 0 { + continue + } + + c := CircuitID{} + for i, k := range re.SubexpNames() { + switch k { + case "slot": + c.Slot = match[i] + case "mod": + c.Module = match[i] + case "port": + c.Port = match[i] + case "subport": + c.SubPort = match[i] + case "vlan": + c.Vlan = match[i] + } + } + + return &c, nil + } + return nil, fmt.Errorf("Unable to match circuit id : %s with listed regexes of interface types", circuitIdStr) +} + +// 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/dhcpv4/ztpv4/parse_circuitid_test.go b/dhcpv4/ztpv4/parse_circuitid_test.go new file mode 100644 index 0000000..b075a16 --- /dev/null +++ b/dhcpv4/ztpv4/parse_circuitid_test.go @@ -0,0 +1,109 @@ +package ztpv4 + +import ( + "testing" + + "github.com/insomniacslk/dhcp/dhcpv4" + "github.com/stretchr/testify/require" +) + +func TestMatchCircuitID(t *testing.T) { + tt := []struct { + name string + circuit string + want *CircuitID + fail bool + }{ + {name: "Bogus string", circuit: "bogus_interface", fail: true, want: nil}, + {name: "juniperQFX pattern", circuit: "et-0/0/0:0.0", want: &CircuitID{Slot: "0", Module: "0", Port: "0", SubPort: "0"}}, + {name: "juniperPTX pattern", circuit: "et-0/0/0.0", want: &CircuitID{Slot: "0", Module: "0", Port: "0", SubPort: "0"}}, + {name: "Arista pattern", circuit: "Ethernet3/17/1", want: &CircuitID{Slot: "3", Module: "17", Port: "1"}}, + {name: "Juniper QFX pattern", circuit: "et-1/0/61", want: &CircuitID{Slot: "1", Module: "0", Port: "61"}}, + {name: "Arista Vlan pattern 1", circuit: "Ethernet14:Vlan2001", want: &CircuitID{Port: "14", Vlan: "Vlan2001"}}, + {name: "Arista Vlan pattern 2", circuit: "Ethernet10:2020", want: &CircuitID{Port: "10", Vlan: "2020"}}, + {name: "Cisco pattern", circuit: "Gi1/10:2020", want: &CircuitID{Slot: "1", Port: "10", Vlan: "2020"}}, + {name: "Cisco Nexus pattern", circuit: "Ethernet1/3", want: &CircuitID{Slot: "1", Port: "3"}}, + {name: "Juniper Bundle Pattern", circuit: "ae52.0", want: &CircuitID{Port: "52", SubPort: "0"}}, + } + 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 CircuitID") + } else { + require.Equal(t, tc.want, circuit, "comparing CircuitID") + } + }) + } +} + +func TestFormatCircuitID(t *testing.T) { + tt := []struct { + name string + circuit *CircuitID + want string + fail bool + }{ + {name: "empty", circuit: &CircuitID{}, want: ",,,,"}, + {name: "juniperQFX pattern", circuit: &CircuitID{Slot: "0", Module: "0", Port: "0", SubPort: "0"}, want: "0,0,0,0,"}, + {name: "juniperPTX pattern", circuit: &CircuitID{Slot: "0", Module: "0", Port: "0"}, want: "0,0,0,,"}, + {name: "Arista pattern", circuit: &CircuitID{Slot: "3", Module: "17", Port: "1"}, want: "3,17,1,,"}, + {name: "Juniper QFX pattern", circuit: &CircuitID{Slot: "1", Module: "0", Port: "61"}, want: "1,0,61,,"}, + {name: "Arista Vlan pattern 1", circuit: &CircuitID{Port: "14", Vlan: "Vlan2001"}, want: ",,14,,Vlan2001"}, + {name: "Arista Vlan pattern 2", circuit: &CircuitID{Port: "10", Vlan: "2020"}, want: ",,10,,2020"}, + {name: "Cisco Nexus pattern", circuit: &CircuitID{Slot: "1", Port: "3"}, want: "1,,3,,"}, + {name: "Juniper Bundle Pattern", circuit: &CircuitID{Port: "52", SubPort: "0"}, want: ",,52,0,"}, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + circuit := tc.circuit.FormatCircuitID() + require.Equal(t, tc.want, circuit, "FormatCircuitID data") + }) + } + +} + +func TestParseCircuitID(t *testing.T) { + tt := []struct { + name string + circuit []byte + want *CircuitID + fail bool + }{ + {name: "Bogus test", circuit: []byte("bogusInterface"), fail: true, want: nil}, + {name: "juniperQFX pattern", circuit: []byte("et-0/0/0:0.0"), want: &CircuitID{Slot: "0", Module: "0", Port: "0", SubPort: "0"}}, + {name: "Bogus string", circuit: []byte("bogus_interface"), fail: true, want: nil}, + {name: "juniperQFX pattern", circuit: []byte("et-0/0/0:0.0"), want: &CircuitID{Slot: "0", Module: "0", Port: "0", SubPort: "0"}}, + {name: "juniperPTX pattern", circuit: []byte("et-0/0/0.0"), want: &CircuitID{Slot: "0", Module: "0", Port: "0", SubPort: "0"}}, + {name: "Arista pattern", circuit: []byte("Ethernet3/17/1"), want: &CircuitID{Slot: "3", Module: "17", Port: "1"}}, + {name: "Juniper QFX pattern", circuit: []byte("et-1/0/61"), want: &CircuitID{Slot: "1", Module: "0", Port: "61"}}, + {name: "Arista Vlan pattern 1", circuit: []byte("Ethernet14:Vlan2001"), want: &CircuitID{Port: "14", Vlan: "Vlan2001"}}, + {name: "Arista Vlan pattern 2", circuit: []byte("Ethernet10:2020"), want: &CircuitID{Port: "10", Vlan: "2020"}}, + {name: "Cisco pattern", circuit: []byte("Gi1/10:2020"), want: &CircuitID{Slot: "1", Port: "10", Vlan: "2020"}}, + {name: "Cisco Nexus pattern", circuit: []byte("Ethernet1/3"), want: &CircuitID{Slot: "1", Port: "3"}}, + {name: "Juniper Bundle Pattern", circuit: []byte("ae52.0"), want: &CircuitID{Port: "52", SubPort: "0"}}, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + + addOption := dhcpv4.WithOption(dhcpv4.OptRelayAgentInfo(dhcpv4.OptGeneric(dhcpv4.GenericOptionCode(1), tc.circuit))) + packet, err := dhcpv4.New(addOption) + if err != nil { + t.Errorf("Unable to create dhcpv4 packet with circuiti") + } + c, err := ParseCircuitID(packet) + if err != nil && !tc.fail { + t.Errorf("Testcase Failed %v", err) + } + if c != nil { + require.Equal(t, *tc.want, *c, "Comparing DHCPv4 Relay Agent Info CircuitId") + } else { + require.Equal(t, tc.want, c, "Comparing DHCPv4 Relay Agent Info CircuitId") + } + }) + } +} |