// 2020 Rackspace, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. // Package compareports will aggregate the switchport configurations on both the source and // the destination ports, as well as provide an optional diff of the configs. package compareports import ( "fmt" "os" "sort" "strconv" "strings" "github.com/gookit/color" "github.com/jedib0t/go-pretty/v6/table" "github.com/sirupsen/logrus" "github.rackspace.com/DCM/gomigrate/shared" "github.rackspace.com/DCM/tools" "github.rackspace.com/gorax/clients/dcxAutohost" "github.rackspace.com/gorax/clients/mongodbAPI" "github.rackspace.com/gorax/clients/unipede" "github.rackspace.com/gorax/gokeys" ) type ( // Client contains all the data types needed to // initialize and use this package. Client struct { ctkToken string raxToken string mgo *mongodbAPI.Client uni *unipede.Client dcx *dcxAutohost.Client } resultType struct { ID int Source portData Migration portData } portData struct { Switch string Type string Port string Ifc string Commands []string } ) // pluginFunc function is executed from the gomigrate caller func pluginFunc(cfg gokeys.Settings) { c := initCfg(cfg) deviceList, err := shared.MongoDevices(nil, nil, *argD, "", c.ctkToken, nil) if err != nil { logrus.Fatal(err) } if len(deviceList) == 0 { logrus.Fatal("No devices defined") } allPortMaps, err := c.portMaps(deviceList) if err != nil { logrus.Fatal(err) } for _, device := range shared.SortDevices(deviceList) { isRackspace := device.Linked.ID == 0 deviceResp := make([]resultType, 0) if _, ok := allPortMaps[device.ID]; !ok { logrus.Errorf("[%d] Unable to find port-maps: %s", device.ID, device.Account.Name) continue } dpm := allPortMaps[device.ID] for _, p := range shared.SortPortMaps(dpm.PortMaps) { r := resultType{ ID: device.ID, Source: portData{ Switch: fmt.Sprintf("%s.%s", p.Source.Switch.Number, dpm.SourceDC), Type: p.Source.SwitchPortType, Port: func() string { if !isRackspace && p.Source.Name != "" { return p.Source.Name } return strconv.Itoa(p.Source.PortNumber) }(), Ifc: func() string { if !isRackspace && p.Source.Name != "" { return p.Source.Name } return "" }(), }, Migration: portData{ Switch: fmt.Sprintf("%s.%s", p.Migration.Switch.Number, dpm.MigrationDC), Type: p.Migration.SwitchPortType, Port: func() string { if !isRackspace && p.Migration.Name != "" { return p.Migration.Name } return strconv.Itoa(p.Migration.PortNumber) }(), Ifc: func() string { if !isRackspace && p.Migration.Name != "" { return p.Migration.Name } return "" }(), }, } if r.Source.Ifc == "" { r.Source.Ifc, err = c.interfaceName(r.Source.Switch, r.Source.Port) if err != nil { logrus.Errorf("[%d] Failed interface name: %s[%d]", device.ID, r.Source.Switch, p.Source.PortNumber) continue } } r.Source.Commands, err = c.interfaceConfig(r.Source.Switch, r.Source.Ifc) if err != nil { logrus.Errorf("[%d] Unable to get interface config: %s[%d]", device.ID, r.Source.Switch, p.Source.PortNumber) continue } if r.Migration.Ifc == "" { r.Migration.Ifc, err = c.interfaceName(r.Migration.Switch, r.Migration.Port) if err != nil { logrus.Errorf("[%d] Unable to get interface name: %s[%d]", device.ID, r.Migration.Switch, p.Migration.PortNumber) continue } } r.Migration.Commands, err = c.interfaceConfig(r.Migration.Switch, r.Migration.Ifc) if err != nil { logrus.Errorf("[%d] Unable to get interface config: %s[%d]", device.ID, r.Migration.Switch, p.Migration.PortNumber) continue } deviceResp = append(deviceResp, r) } pPrint(deviceResp, device) } } // pPrint func will take a structured type of response data and render a table // output to stdout. func pPrint(data []resultType, device tools.Device) { t := table.NewWriter() t.SetOutputMirror(os.Stdout) header := table.Row{fmt.Sprintf("%d: Source", device.ID), "Migration"} if *argB { header = append(header, "Diff") } t.AppendHeader(header) for _, v := range data { row := table.Row{ fmt.Sprintf("%s[%s] (%s)\n%s\n %s\n", v.Source.Switch, v.Source.Port, v.Source.Type, v.Source.Ifc, strings.Join(v.Source.Commands, "\n ")), fmt.Sprintf("%s[%s] (%s)\n%s\n %s\n", v.Migration.Switch, v.Migration.Port, v.Migration.Type, v.Migration.Ifc, strings.Join(v.Migration.Commands, "\n ")), } if *argB { row = append(row, fmt.Sprintf("\n\n%s\n", strings.Join(diff(v.Source.Commands, v.Migration.Commands), "\n"))) } t.AppendRow(row) } t.SortBy([]table.SortBy{{Name: fmt.Sprintf("%d: Source Config", device.ID), Mode: table.Dsc}}) t.SetStyle(table.StyleDefault) t.Render() } // interfaceName function will get the real interface name for a given fe_port func (c Client) interfaceName(switchName string, portName string) (string, error) { intName := "" filter := fmt.Sprintf("name:=%s", portName) if n, err := strconv.Atoi(portName); err == nil { filter = fmt.Sprintf("fe_port:=%d", n) } upInt, err := c.uni.UpInterfaces(switchName, argA, &unipede.UnipedeOptions{Filter: &filter}) if err != nil { logrus.Errorf("interfaceName:%s", err) return intName, err } if len(upInt.Items) > 1 { return intName, fmt.Errorf("interfaceName:multiple ifcs found for %s[%s]", switchName, portName) } return upInt.Items[0].Name, nil } // interfaceConfig function will get the configuration lines for a specified interface. func (c Client) interfaceConfig(switchName, intName string) ([]string, error) { filter := fmt.Sprintf("context:=interface %s", intName) cfgLines, err := c.uni.ConfigLine(switchName, argA, &unipede.UnipedeOptions{Filter: &filter}) if err != nil { logrus.Errorf("interfaceConfig:%s", err) return nil, err } commands := make([]string, 0) for _, line := range cfgLines { commands = append(commands, line.Command) } sort.Strings(commands) return commands, nil } // portMaps function will retrieve the device port-maps from the DCX API func (c Client) portMaps(devices []tools.Device) (map[int]dcxAutohost.DevicePortMap, error) { deviceList := make([]int, 0) for _, d := range devices { deviceList = append(deviceList, d.ID) } retVal := make(map[int]dcxAutohost.DevicePortMap) for _, d := range deviceList { dpm, err := shared.PortMapsForDevice(d, c.raxToken) if err != nil { logrus.Errorf("interfaceName:%s", err) return nil, err } retVal[d] = dpm } return retVal, nil } // initCfg is the initialization method for this package that will populate an // instance of the Client struct for subsequent calls to its methods. func initCfg(cfg gokeys.Settings) *Client { // Pull an identity token from the users gokeys keychain iKey, err := cfg.Identity(false) if err != nil { logrus.Fatal(err) } cKey, err := cfg.Core(false) if err != nil { logrus.Fatal(err) } sKey, err := cfg.SSO() if err != nil { logrus.Fatal(err) } dcx, err := dcxAutohost.NewWithToken(iKey.Password, false) if err != nil { logrus.Fatal(err) } uni, err := unipede.New(sKey.Username, sKey.Password, "") if err != nil { logrus.Fatal(err) } return &Client{ mgo: mongodbAPI.New(mongodbAPI.WithToken(cKey.Password), "migration", false), uni: uni, dcx: dcx, ctkToken: cKey.Password, raxToken: iKey.Password, } } func diff(slice1 []string, slice2 []string) []string { var retVal []string for _, s1 := range slice1 { found := false for _, s2 := range slice2 { if s1 == s2 { found = true break } } if !found { retVal = append(retVal, color.Green.Sprintf("+%s", s1)) } } for _, s2 := range slice2 { found := false for _, s1 := range slice1 { if s2 == s1 { found = true break } } if !found { retVal = append(retVal, color.Red.Sprintf("-%s", s2)) } } sort.Strings(retVal) return retVal }