diff --git a/pkg/connector/netbox/netbox.go b/pkg/connector/netbox/netbox.go index 1ada723..56086d0 100644 --- a/pkg/connector/netbox/netbox.go +++ b/pkg/connector/netbox/netbox.go @@ -10,13 +10,13 @@ package netbox import ( "fmt" "regexp" - "strconv" "strings" "sync" "sync/atomic" "time" dbModel "github.com/cloudflare/octopus/pkg/connector/netbox/model" + nbUtils "github.com/cloudflare/octopus/pkg/connector/netbox/utils" "github.com/cloudflare/octopus/pkg/model" "github.com/cloudflare/octopus/proto/octopus" @@ -192,7 +192,7 @@ func (n *NetboxConnector) addDevices(t *model.Topology) error { topoDev.Role = d.DeviceRole.Slug topoDev.DeviceType = d.DeviceType.Slug - md, err := metaDataFromTags(d.Tags) + md, err := nbUtils.GetMetaDataFromTags(d.Tags) if err != nil { return fmt.Errorf("unable to get meta data: %v", err) } @@ -218,7 +218,7 @@ func (n *NetboxConnector) addInterfaces(t *model.Topology) error { t.DevicesByInterfaceID[nbIfa.ID] = d t.Interfaces[nbIfa.ID] = ifa - md, err := metaDataFromTags(nbIfa.Tags) + md, err := nbUtils.GetMetaDataFromTags(nbIfa.Tags) if err != nil { return fmt.Errorf("unable to get meta data: %v", err) } @@ -255,7 +255,7 @@ func (n *NetboxConnector) addInterfaceUnits(t *model.Topology) error { return fmt.Errorf("can not find interface %s:%s", nbIfa.Device.Name, nbIfa.Parent.Name) } - vlanTag, err := parseUnitStr(unitStr) + vlanTag, err := nbUtils.ParseUnitStr(unitStr) if err != nil { return fmt.Errorf("Unable to convert unit %q (id=%d) (interface %q) to int for %s:%s. Ignoring logical interface.", unitStr, nbIfa.ID, nbIfa.Name, nbIfa.Device.Name, nbIfa.Parent.Name) } @@ -263,7 +263,7 @@ func (n *NetboxConnector) addInterfaceUnits(t *model.Topology) error { u := ifa.AddUnitIfNotExists(vlanTag) t.DevicesByInterfaceID[nbIfa.ID] = d - md, err := metaDataFromTags(nbIfa.Tags) + md, err := nbUtils.GetMetaDataFromTags(nbIfa.Tags) if err != nil { return fmt.Errorf("unable to get meta data: %v", err) } @@ -274,34 +274,6 @@ func (n *NetboxConnector) addInterfaceUnits(t *model.Topology) error { return nil } -func parseUnitStr(unitStr string) (model.VLANTag, error) { - if strings.Contains(unitStr, ".") { - parts := strings.Split(unitStr, ".") - if len(parts) != 2 { - return model.VLANTag{}, fmt.Errorf("invalid unit string %q", unitStr) - } - - outerTag, err := strconv.Atoi(parts[0]) - if err != nil { - return model.VLANTag{}, fmt.Errorf("unable to convert %q to int: %v", parts[0], err) - } - - innerTag, err := strconv.Atoi(parts[1]) - if err != nil { - return model.VLANTag{}, fmt.Errorf("unable to convert %q to int: %v", parts[1], err) - } - - return model.NewVLANTag(uint16(outerTag), uint16(innerTag)), nil - } - - ctag, err := strconv.Atoi(unitStr) - if err != nil { - return model.VLANTag{}, fmt.Errorf("unable to convert %q to int: %v", unitStr, err) - } - - return model.NewVLANTag(0, uint16(ctag)), nil -} - func (n *NetboxConnector) addIPAddresses(t *model.Topology) error { for _, nbIP := range n.ipAddresses { if nbIP.AssignedObjectID == 0 || nbIP.AssignedObjectTypeID != n.client.GetDcimInterfaceTypeID() { @@ -323,18 +295,18 @@ func (n *NetboxConnector) addIPAddresses(t *model.Topology) error { return fmt.Errorf("interface with id %d not found", ifaID) } - _, vt, err := getInterfaceAndVLANTag(dcimIfa.Name) + _, vt, err := nbUtils.GetInterfaceAndVLANTag(dcimIfa.Name) if err != nil { return fmt.Errorf("unable to extract interface name and unit from %q: %v", dcimIfa.Name, err) } - pfx, err := bnet.PrefixFromString(sanitizeIPAddress(nbIP.Address)) + pfx, err := bnet.PrefixFromString(nbUtils.SanitizeIPAddress(nbIP.Address)) if err != nil { return fmt.Errorf("failed to parse IP %q: %v", nbIP.Address, err) } ip := model.NewIP(*pfx) - getCustomFieldData(ip.MetaData, nbIP.CustomFieldData) + nbUtils.GetCustomFieldData(ip.MetaData, nbIP.CustomFieldData) u := ifa.AddUnitIfNotExists(vt) if pfx.Addr().IsIPv4() { @@ -347,18 +319,6 @@ func (n *NetboxConnector) addIPAddresses(t *model.Topology) error { return nil } -func sanitizeIPAddress(addr string) string { - if strings.Contains(addr, "/") { - return addr - } - - if strings.Contains(addr, ".") { - return addr + "/32" - } - - return addr + "/128" -} - func (n *NetboxConnector) addPrefixes(t *model.Topology) error { for _, p := range n.prefixes { pfx, err := bnet.PrefixFromString(p.Prefix) @@ -366,7 +326,7 @@ func (n *NetboxConnector) addPrefixes(t *model.Topology) error { return fmt.Errorf("failed to parse Prefix %q: %v", p.Prefix, err) } - md, err := metaDataFromTags(p.Tags) + md, err := nbUtils.GetMetaDataFromTags(p.Tags) if err != nil { return fmt.Errorf("failed to get Tags for Prefix %q: %v", p.Prefix, err) } @@ -380,36 +340,6 @@ func (n *NetboxConnector) addPrefixes(t *model.Topology) error { return nil } -func metaDataFromTags(tags []string) (*model.MetaData, error) { - ret := model.NewMetaData() - for _, tag := range tags { - parts := strings.Split(tag, "=") - - // Semantic Tag - if len(parts) == 2 { - if _, exists := ret.SemanticTags[parts[0]]; exists { - return nil, fmt.Errorf("Key %q exists already: %q vs. %q", parts[0], ret.SemanticTags[parts[0]], parts[1]) - } - - ret.SemanticTags[parts[0]] = parts[1] - - } else { - // Regular Tag - ret.Tags = append(ret.Tags, tag) - } - } - - return ret, nil -} - -func getCustomFieldData(md *model.MetaData, customFieldData string) { - if customFieldData == "" || customFieldData == "{}" { - return - } - - md.CustomFieldData = customFieldData -} - func (n *NetboxConnector) getCableEnd(terminationType int32, terminationID int64, t *model.Topology) (*model.CableEnd, error) { ce := model.CableEnd{} @@ -573,62 +503,6 @@ func (n *NetboxConnector) addFrontPorts(t *model.Topology) error { return nil } -func getInterfaceAndVLANTag(name string) (ifName string, vt model.VLANTag, err error) { - if isLogicalInterface(name) { - return extractInterfaceAndUnit(name) - } - - return name, model.VLANTag{}, nil -} - -func isLogicalInterface(name string) bool { - return strings.Contains(name, ".") -} - -func extractInterfaceAndUnit(name string) (string, model.VLANTag, error) { - extractedVars := reSubMatchMap(ifNameTagsRegexp, name) - if _, exists := extractedVars["intf_name"]; !exists { - return "", model.VLANTag{}, fmt.Errorf("unable to extract interface name from %q", name) - } - - if _, exists := extractedVars["inner_tag"]; !exists { - return "", model.VLANTag{}, fmt.Errorf("unable to extract inner tag from %q", name) - } - - innerTag, err := strconv.Atoi(extractedVars["inner_tag"]) - if err != nil { - return "", model.VLANTag{}, fmt.Errorf("unable to convert inner tag from string %q to int (%q)", extractedVars["inner_tag"], name) - } - - vt := model.VLANTag{ - InnerTag: uint16(innerTag), - } - - outerTagStr := extractedVars["outer_tag"] - if outerTagStr != "" { - outerTag, err := strconv.Atoi(outerTagStr) - if err != nil { - return "", model.VLANTag{}, fmt.Errorf("unable to convert outer tag from string %q to int (%q)", outerTagStr, name) - } - - vt.OuterTag = uint16(outerTag) - } - - return extractedVars["intf_name"], vt, nil -} - -func reSubMatchMap(r *regexp.Regexp, str string) map[string]string { - match := r.FindStringSubmatch(str) - subMatchMap := make(map[string]string) - for i, name := range r.SubexpNames() { - if i != 0 && name != "" && i <= len(match) { - subMatchMap[name] = match[i] - } - } - - return subMatchMap -} - func (n *NetboxConnector) StartRefreshRoutine() { go n.refreshRoutine() } diff --git a/pkg/connector/netbox/netbox_test.go b/pkg/connector/netbox/netbox_test.go index 2f974d0..53cc4a1 100644 --- a/pkg/connector/netbox/netbox_test.go +++ b/pkg/connector/netbox/netbox_test.go @@ -16,6 +16,7 @@ import ( bnet "github.com/bio-routing/bio-rd/net" dbModel "github.com/cloudflare/octopus/pkg/connector/netbox/model" + nbUtils "github.com/cloudflare/octopus/pkg/connector/netbox/utils" octopuspb "github.com/cloudflare/octopus/proto/octopus" ) @@ -1051,7 +1052,7 @@ func TestExtractInterfaceAndUnit(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - ifName, vt, err := extractInterfaceAndUnit(test.input) + ifName, vt, err := nbUtils.ExtractInterfaceAndUnit(test.input) if err != nil { if test.wantFail { return diff --git a/pkg/connector/netbox/utils/utils.go b/pkg/connector/netbox/utils/utils.go new file mode 100644 index 0000000..bb52360 --- /dev/null +++ b/pkg/connector/netbox/utils/utils.go @@ -0,0 +1,140 @@ +package utils + +import ( + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/cloudflare/octopus/pkg/model" +) + +var ( + ifNameTagsRegexp = regexp.MustCompile(`^(?P.+?)\.((?P\d+)\.)?(?P\d+)$`) +) + +func ParseUnitStr(unitStr string) (model.VLANTag, error) { + if strings.Contains(unitStr, ".") { + parts := strings.Split(unitStr, ".") + if len(parts) != 2 { + return model.VLANTag{}, fmt.Errorf("invalid unit string %q", unitStr) + } + + outerTag, err := strconv.Atoi(parts[0]) + if err != nil { + return model.VLANTag{}, fmt.Errorf("unable to convert %q to int: %v", parts[0], err) + } + + innerTag, err := strconv.Atoi(parts[1]) + if err != nil { + return model.VLANTag{}, fmt.Errorf("unable to convert %q to int: %v", parts[1], err) + } + + return model.NewVLANTag(uint16(outerTag), uint16(innerTag)), nil + } + + ctag, err := strconv.Atoi(unitStr) + if err != nil { + return model.VLANTag{}, fmt.Errorf("unable to convert %q to int: %v", unitStr, err) + } + + return model.NewVLANTag(0, uint16(ctag)), nil +} + +func SanitizeIPAddress(addr string) string { + if strings.Contains(addr, "/") { + return addr + } + + if strings.Contains(addr, ".") { + return addr + "/32" + } + + return addr + "/128" +} + +func GetMetaDataFromTags(tags []string) (*model.MetaData, error) { + ret := model.NewMetaData() + for _, tag := range tags { + parts := strings.Split(tag, "=") + + // Semantic Tag + if len(parts) == 2 { + if _, exists := ret.SemanticTags[parts[0]]; exists { + return nil, fmt.Errorf("key %q exists already: %q vs. %q", parts[0], ret.SemanticTags[parts[0]], parts[1]) + } + + ret.SemanticTags[parts[0]] = parts[1] + + } else { + // Regular Tag + ret.Tags = append(ret.Tags, tag) + } + } + + return ret, nil +} + +func GetCustomFieldData(md *model.MetaData, customFieldData string) { + if customFieldData == "" || customFieldData == "{}" { + return + } + + md.CustomFieldData = customFieldData +} + +func GetInterfaceAndVLANTag(name string) (ifName string, vt model.VLANTag, err error) { + if isLogicalInterface(name) { + return ExtractInterfaceAndUnit(name) + } + + return name, model.VLANTag{}, nil +} + +func isLogicalInterface(name string) bool { + return strings.Contains(name, ".") +} + +func ExtractInterfaceAndUnit(name string) (string, model.VLANTag, error) { + extractedVars := reSubMatchMap(ifNameTagsRegexp, name) + if _, exists := extractedVars["intf_name"]; !exists { + return "", model.VLANTag{}, fmt.Errorf("unable to extract interface name from %q", name) + } + + if _, exists := extractedVars["inner_tag"]; !exists { + return "", model.VLANTag{}, fmt.Errorf("unable to extract inner tag from %q", name) + } + + innerTag, err := strconv.Atoi(extractedVars["inner_tag"]) + if err != nil { + return "", model.VLANTag{}, fmt.Errorf("unable to convert inner tag from string %q to int (%q)", extractedVars["inner_tag"], name) + } + + vt := model.VLANTag{ + InnerTag: uint16(innerTag), + } + + outerTagStr := extractedVars["outer_tag"] + if outerTagStr != "" { + outerTag, err := strconv.Atoi(outerTagStr) + if err != nil { + return "", model.VLANTag{}, fmt.Errorf("unable to convert outer tag from string %q to int (%q)", outerTagStr, name) + } + + vt.OuterTag = uint16(outerTag) + } + + return extractedVars["intf_name"], vt, nil +} + +func reSubMatchMap(r *regexp.Regexp, str string) map[string]string { + match := r.FindStringSubmatch(str) + subMatchMap := make(map[string]string) + for i, name := range r.SubexpNames() { + if i != 0 && name != "" && i <= len(match) { + subMatchMap[name] = match[i] + } + } + + return subMatchMap +}