package portstatus import ( "fmt" "net/url" "os" "sort" "strconv" "strings" "sync" "github.com/gookit/color" "github.com/jedib0t/go-pretty/v6/table" "github.com/sirupsen/logrus" "github.rackspace.com/DCM/gomigrate/core" "github.rackspace.com/DCM/gomigrate/shared" "github.rackspace.com/DCM/tools" "github.rackspace.com/gorax/clients/unipede" "github.rackspace.com/gorax/gokeys" ) var ( argDev *[]int argSrc *bool argMig *bool argAge *int argNoConfig *bool argNoMacs *bool argNoFallback *bool argThreads *int // limit concurrent network I/O to Unipede, so we don’t overwhelm it or the network netSem chan struct{} ) // Plugin function: PortStatus plugin function for fetching port status & config of devices simultaneously func Plugin(p *core.Parser) core.Plugin { cmd := p.NewCommand("portstatus", "Print port config, MAC table, and status (source, migration, or both)") argDev = shared.ArgDevices(cmd) argSrc = shared.ArgFlag(cmd, "src", "Include source ports") argMig = shared.ArgFlag(cmd, "mig", "Include migration ports") argAge = shared.ArgUnipedeAge(cmd, 0) // 24h default argNoConfig = shared.ArgFlag(cmd, "no-config", "Skip interface configuration lookup") argNoMacs = shared.ArgFlag(cmd, "address-skip", "Don't fetch MAC addresses") argNoFallback = shared.ArgFlag(cmd, "exclude-fallback", "Don't list all interfaces when fe_port lookup fails") argThreads = shared.ArgRoutines(cmd, 8) // -t, --threads (default 8) return core.Plugin{CMD: cmd, Func: pluginFunc} } func pluginFunc(cfg gokeys.Settings) { fmt.Println(color.New(color.FgBlue, color.Bold).Render("Running portstatus…")) if argDev == nil || len(*argDev) == 0 { logrus.Fatal("Device ID is required (-d or --devices)") } // Initialize the network semaphore using --threads (clamped 1..32) if netSem == nil { n := 8 if argThreads != nil { n = *argThreads } if n < 1 { n = 1 } else if n > 32 { n = 32 } netSem = make(chan struct{}, n) } // Fetch credentials identity, err := cfg.Identity(false) // for PortMaps if err != nil { logrus.Fatal(err) } coreKey, err := cfg.Core(false) // for device type if err != nil { logrus.Fatal(err) } sso, err := cfg.SSO() // for Unipede if err != nil { logrus.Fatal(err) } // clients uni, err := unipede.New(sso.Username, sso.Password, "") if err != nil { logrus.Fatal(err) } // if neither source nor migration flag is set, default to both if !*argSrc && !*argMig { *argSrc, *argMig = true, true } // pulls device port mapping of source -> migration ports from the DCX API. for _, deviceID := range *argDev { portMap, err := shared.PortMapsForDevice(deviceID, identity.Password) if err != nil { logrus.Errorf("[%d] port map error: %v", deviceID, err) continue } //Gets device type using mongoDB devType := strings.ToLower(getDeviceType(deviceID, coreKey.Password)) if devType == "" { devType = "UNKNOWN" } else { devType = strings.ToUpper(devType) } // Prefetch MAC tables in parallel for this device (source+migration sides) macCache := map[string]string{} if !*argNoMacs { seen := make(map[string]struct{}) type macTask struct { Switch string Port int } var tasks []macTask for _, pm := range portMap.PortMaps { if *argSrc { sw := fmt.Sprintf("%s.%s", pm.Source.Switch.Number, portMap.SourceDC) key := taskKey(sw, pm.Source.PortNumber) if _, ok := seen[key]; !ok { seen[key] = struct{}{} tasks = append(tasks, macTask{Switch: sw, Port: pm.Source.PortNumber}) } } if *argMig { sw := fmt.Sprintf("%s.%s", pm.Migration.Switch.Number, portMap.MigrationDC) key := taskKey(sw, pm.Migration.PortNumber) if _, ok := seen[key]; !ok { seen[key] = struct{}{} tasks = append(tasks, macTask{Switch: sw, Port: pm.Migration.PortNumber}) } } } // Worker pool to fetch all MACs, respects netSem for rate limiting. result channel (resCh) if len(tasks) > 0 { type macResult struct { Key string Val string } taskCh := make(chan macTask) resCh := make(chan macResult) var wg sync.WaitGroup workers := cap(netSem) if workers < 1 { workers = 1 } else if workers > 32 { workers = 32 } // start workers for w := 0; w < workers; w++ { wg.Add(1) go func() { defer wg.Done() for t := range taskCh { val := getPortMACs(uni, t.Switch, t.Port, argAge) resCh <- macResult{Key: taskKey(t.Switch, t.Port), Val: val} } }() } // feed tasks go func() { for _, t := range tasks { taskCh <- t } close(taskCh) }() // closer for resCh after all workers finish go func() { wg.Wait() close(resCh) }() // collect results until channel closes for r := range resCh { macCache[r.Key] = r.Val } } } // table t := table.NewWriter() t.SetOutputMirror(os.Stdout) t.SetStyle(table.StyleLight) // single row above the headers for device number. Added device type (ALL CAPS): "1024426: FIREWALL" t.SetTitle(fmt.Sprintf("%d: %s", deviceID, devType)) // header header := table.Row{} if *argSrc { header = append(header, "SOURCE", "STATUS", "ADMIN", "MAC") } if *argMig { header = append(header, "MIGRATION", "STATUS", "ADMIN", "MAC") } t.AppendHeader(header) // rows for _, pm := range portMap.PortMaps { // pm = port map row := table.Row{} //gather source switchport info if *argSrc { sw := fmt.Sprintf("%s.%s", pm.Source.Switch.Number, portMap.SourceDC) // sw = switch si := gatherPortInfo(uni, sw, pm.Source.PortNumber, pm.Source.SwitchPortType, *argNoConfig) // si = source info if !*argNoMacs { // filled from prefetch cache; no network I/O here si.MACs = macCache[taskKey(sw, pm.Source.PortNumber)] } //switchport operational/admin states default red unless "up and "enabled' statusStyle := color.FgRed if strings.EqualFold(si.Status, "up") { statusStyle = color.FgGreen } adminStyle := color.FgRed if strings.EqualFold(si.Admin, "enabled") { adminStyle = color.FgGreen } row = append(row, si.Block, color.Style{statusStyle}.Render(si.Status), color.Style{adminStyle}.Render(si.Admin), si.MACs, ) } //gather migration switchport info if *argMig { sw := fmt.Sprintf("%s.%s", pm.Migration.Switch.Number, portMap.MigrationDC) mi := gatherPortInfo(uni, sw, pm.Migration.PortNumber, pm.Migration.SwitchPortType, *argNoConfig) if !*argNoMacs { // filled from prefetch cache; no network I/O here mi.MACs = macCache[taskKey(sw, pm.Migration.PortNumber)] } statusStyle := color.FgRed if strings.EqualFold(mi.Status, "up") { statusStyle = color.FgGreen } adminStyle := color.FgRed if strings.EqualFold(mi.Admin, "enabled") { adminStyle = color.FgGreen } row = append(row, mi.Block, color.Style{statusStyle}.Render(mi.Status), color.Style{adminStyle}.Render(mi.Admin), mi.MACs, ) } //append the completed row and add a separator line between port mapping t.AppendRow(row) t.AppendSeparator() } t.Render() } } // portInfo all the data we need for each side src or mig port type portInfo struct { Block string Status string Admin string MACs string } // gatherPortInfo pulls from unipede: switch, interface, interface type, config, status, admin, (and now MAC) func gatherPortInfo(uni *unipede.Client, switchFQDN string, fePort int, portType string, skipConfig bool) portInfo { //fetch only the matching interface by fe_port (front end port, not fast eth) filter := fmt.Sprintf("fe_port:=%d", fePort) netSem <- struct{}{} up, err := uni.UpInterfaces(switchFQDN, nil, &unipede.UnipedeOptions{Filter: &filter}) <-netSem //if fails or no items, fall back to fetching all interfaces on switch (unless --no-fallback) if (err != nil || len(up.Items) == 0) && !*argNoFallback { netSem <- struct{}{} up, err = uni.UpInterfaces(switchFQDN, nil, nil) <-netSem //if no ports found, log and print portInfo with "no interfaces" if err != nil || len(up.Items) == 0 { logrus.Warnf("No interfaces for %s[%d]: %v", switchFQDN, fePort, err) return portInfo{Block: fmt.Sprintf("%s[%d]\n", switchFQDN, fePort), Status: "?", Admin: "?", MACs: ""} } } else if err != nil || len(up.Items) == 0 { logrus.Warnf("No interfaces for %s[%d]: %v", switchFQDN, fePort, err) return portInfo{Block: fmt.Sprintf("%s[%d]\n", switchFQDN, fePort), Status: "?", Admin: "?", MACs: ""} } // match by FePort first, then by name-suffix var intf *unipede.UpInterface suffix := fmt.Sprintf("/%d", fePort) for _, it := range up.Items { if it.FePort != nil && *it.FePort == fePort { intf = it break } if strings.HasSuffix(it.Name, suffix) && intf == nil { intf = it } } if intf == nil { logrus.Warnf("No matching interface for %s[%d]", switchFQDN, fePort) return portInfo{Block: fmt.Sprintf("%s[%d]\n", switchFQDN, fePort), Status: "?", Admin: "?", MACs: ""} } // config lines. If -n or --no-config flag set, this part is skipped and saves time returning output var cfgLines []string if !skipConfig { ctx := fmt.Sprintf("context:=interface %s", intf.Name) netSem <- struct{}{} lines, err := uni.ConfigLine(switchFQDN, nil, &unipede.UnipedeOptions{Filter: &ctx}) <-netSem if err != nil { logrus.Errorln("Error fetching config lines:", err) return portInfo{Block: fmt.Sprintf("%s[%d]\n", switchFQDN, fePort), Status: "?", Admin: "?", MACs: ""} } cfgLines = make([]string, len(lines)) for i, l := range lines { cfgLines[i] = l.Command } sort.Strings(cfgLines) } header := fmt.Sprintf("%s[%d] (%s)", switchFQDN, fePort, portType) body := append([]string{header, intf.Name}, cfgLines...) return portInfo{Block: strings.Join(body, "\n"), Status: intf.Link, Admin: intf.Admin} } // getPortMACs fetches MACs from Unipede MAC table and returns one line per entry. // - Keeps legitimate duplicates across different VLANs func getPortMACs(uni *unipede.Client, sw string, port int, age *int) string { filter := fmt.Sprintf("fe_port:=%d", port) vals := url.Values{"filter": []string{filter}} // 1) Fast path: ask Unipede for current (no age) netSem <- struct{}{} macView, err := uni.MacAssignmentView(vals, sw, nil) <-netSem // 2) If mac is empty AND there was no age called, try aged if (err != nil || len(macView.Items) == 0) && age != nil && *age > 0 { netSem <- struct{}{} macView, err = uni.MacAssignmentView(vals, sw, age) <-netSem } if err != nil || len(macView.Items) == 0 { return "No MACs Found" } // sort all VLAN:MAC pairs, exclude exact duplicates) type pair struct{ vlan, mac string } pairs := make([]pair, 0, len(macView.Items)) for _, it := range macView.Items { mac := strings.ToLower(strings.TrimSpace(it.Mac)) if mac == "" { continue } vlan := strings.TrimSpace(fmt.Sprint(it.Vlan)) // works for int or string if vlan == "0" { vlan = "" // treat 0 as “no VLAN” } pairs = append(pairs, pair{vlan: vlan, mac: mac}) } if len(pairs) == 0 { return "No MACs Found" } // exclude exact (VLAN,MAC) duplicates seen := make(map[string]struct{}, len(pairs)) uniq := pairs[:0] for _, p := range pairs { k := p.vlan + "\x00" + p.mac if _, ok := seen[k]; ok { continue } seen[k] = struct{}{} uniq = append(uniq, p) } // Sort by VLAN, then MAC sort.Slice(uniq, func(i, j int) bool { vi, vj := uniq[i].vlan, uniq[j].vlan if vi == "" && vj != "" { return true } if vi != "" && vj == "" { return false } if vi == "" && vj == "" { return uniq[i].mac < uniq[j].mac } iv, ie := strconv.Atoi(vi) jv, je := strconv.Atoi(vj) if ie == nil && je == nil && iv != jv { return iv < jv } if vi != vj { return vi < vj } return uniq[i].mac < uniq[j].mac }) //format one entry per line. "VLAN : " or just when VLAN is empty. out := make([]string, len(uniq)) for i, p := range uniq { if p.vlan != "" { out[i] = fmt.Sprintf("VLAN %s: %s", p.vlan, p.mac) } else { out[i] = p.mac } } return strings.Join(out, "\n") } // getDeviceType: uses mongoDB to figure out which collection the device lives in to pull device type. func getDeviceType(deviceID int, coreToken string) string { collection := shared.GetDeviceCollection(deviceID, coreToken) if collection == "" { return "" } devices, err := shared.MongoDevices([]string{}, nil, []int{deviceID}, collection, coreToken, map[string]interface{}{}) if err != nil || len(devices) == 0 { return "" } var dev tools.Device for _, d := range devices { if d.ID == deviceID { dev = d break } } if dev.ID == 0 { dev = devices[0] } return strings.TrimSpace(dev.Device.Type) } // taskKey makes a cache key for switch+port (used for MAC prefetch) func taskKey(sw string, port int) string { return fmt.Sprintf("%s|%d", sw, port) }