// 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 validate import ( "fmt" "log" "os" "strconv" "strings" "sync" "github.com/jokelyo/argparse" "github.com/360EntSecGroup-Skylar/excelize" "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/dcxAutohost" "github.rackspace.com/gorax/clients/migrationAPI" "github.rackspace.com/gorax/gokeys" ) type resultType struct { audit map[string][]string portMap dcxAutohost.DevicePortMap model string device int isRackspace bool rawAudit *migrationAPI.AuditResponse vlanMap []dcxAutohost.EnvironmentMigrationVlan } const ( argA = "age" argC = "csv" argD = "devices" argH = "hyp-match-source" argM = "move-groups" argR = "routines" ) var args = map[string]interface{}{ argA: nil, argD: nil, argH: nil, argM: nil, argC: nil, argR: nil, } // Plugin function will return a argparse.Command type back to the parent parser // nolint:typecheck func Plugin(p *core.Parser) core.Plugin { // Create a argument for the DCX Translations logic cmd := p.NewCommand("validate", "Audit migration port configurations of device") args[argA] = shared.ArgUnipedeAge(cmd, 86400) args[argD] = shared.ArgDevices(cmd) args[argH] = cmd.Flag("H", argH, &argparse.Options{Help: "compare hyp ports to source rather than defaults"}) args[argM] = shared.ArgMoveGroups(cmd) args[argR] = shared.ArgRoutines(cmd, 5) args[argC] = shared.ArgFlag(cmd, argC, "Create a CSV results file") return core.Plugin{CMD: cmd, Func: pluginFunc} } // pluginFunc function is executed from the gomigrate caller and will run the migration_ports_config API // call to validate source/destination switchport mappings. func pluginFunc(cfg gokeys.Settings) { // Set all the key from the arg parser var ( moveGroupsArg = *args[argM].(*[]string) devicesArg = *args[argD].(*[]int) unipedeInt = *args[argA].(*int) hypMatchSource = *args[argH].(*bool) csvBool = *args[argC].(*bool) routinesArg = *args[argR].(*int) ) // Pull a core token from the gokeys keychain coreKey, err := cfg.Core(false) if err != nil { log.Fatal(err) } // Get the device list from a list of move-groups or devices. devices, err := shared.MongoDevices(moveGroupsArg, nil, devicesArg, "", coreKey.Password, nil) if err != nil { logrus.Fatal(err) } // Stop here if we did not find any devices. if len(devices) == 0 { logrus.Fatalf("no devices found in move-groups:(%v) or devices:(%v)", moveGroupsArg, devicesArg) } // Creating a buffered channel that will block when we have reached the // passed in limit for concurrency semChan := make(chan struct{}, routinesArg) // Create a non-blocking channel that will receive all the responses // back from validate. resChan := make(chan *resultType) errChan := make(chan error) // Close the channels when this function has exited. defer func() { close(semChan) close(resChan) close(errChan) }() // Create a map of vlan translations, so we can get translations, but only // once per environment; define mutex to prevent concurrent map write errors var mapLock sync.RWMutex envXlates := make(map[string][]dcxAutohost.EnvironmentMigrationVlan) logrus.Infof("Running validate on %d devices. Running %d processes concurrently.", len(devices), routinesArg) for _, device := range shared.SortDevices(devices) { go func(device tools.Device, mapLock *sync.RWMutex) { // Send an empty struct into the channel that will add the struct to // the limit of the channel length. If the limit is reached, this channel // will block here. semChan <- struct{}{} defer func() { // Now that we're done, pull an empty struct from the channel, // freeing up one spot for another routine to take. <-semChan }() // Pull an identity token from the users gokeys keychain key, err := cfg.Identity(false) if err != nil { log.Fatal(err) } // Grab the client for the migration API c := migrationAPI.New(key.Password, false) var res = resultType{ device: device.ID, model: device.Device.Type, audit: make(map[string][]string), isRackspace: device.Linked.ID == 0, } mapLock.RLock() _, exists := envXlates[device.Environment.Number] mapLock.RUnlock() if !exists && device.Environment.Number != "" { vlans, err := shared.VlansForEnvironment(device.Environment.Number, key.Password) mapLock.Lock() envXlates[device.Environment.Number] = vlans mapLock.Unlock() if err != nil { errChan <- fmt.Errorf("[%d] %s", device.ID, err) } } mapLock.RLock() res.vlanMap = envXlates[device.Environment.Number] mapLock.RUnlock() res.rawAudit, err = c.AuditMigrationPortConfigs( shared.GetDeviceCollection(device.ID, coreKey.Password), device.ID, migrationAPI.NewOptions().SetUnipedeAge(unipedeInt).SetHypMatchSource(hypMatchSource), ) if err != nil { errMsg := "" if res.rawAudit.Error != nil { errMsg = *res.rawAudit.Error } errChan <- fmt.Errorf("[%d]AuditMigrationPortConfigs:%s: %s", device.ID, err, errMsg) return } res.portMap, err = shared.PortMapsForDevice(device.ID, key.Password) if err != nil { errChan <- fmt.Errorf("[%d]PortMapsForDevice:%s", device.ID, err) return } for _, v := range res.rawAudit.Data { k := fmt.Sprintf("%s => %s", v.Current, v.Destination) res.audit[k] = append(res.audit[k], v.Reason) if res.model == "" { res.model = v.TeamName } } // now we can send the result struct through the resultsChan resChan <- &res }(device, &mapLock) } // Make a list of resultTypes to store the results we are about to receive. var results []resultType // Receive the results from the resChan as the come in. for i := 0; i < len(devices); i++ { select { case result := <-resChan: results = append(results, *result) pPrint(*result) case err := <-errChan: logrus.Error(err) } } // If the csv flag is set, call the csv() function to write // the results to a file. if csvBool { if err := csv(results, "Validate", "Sheet1"); err != nil { logrus.Error(err) } } } // pPrint func will take a structured type of response data and render a table // output to stdout. func pPrint(data resultType) { t := table.NewWriter() t.SetOutputMirror(os.Stdout) t.AppendHeader(table.Row{fmt.Sprintf("%d: %s", data.device, data.model)}) var ( tableSpace = 60 portMaps = []string{"PORT MAPS"} vlanMaps = []string{"VLAN MAPS"} errors = make([]string, 0) ) for _, v := range data.vlanMap { if !validVLANMap(v) { continue } if v.MigrationVlan.ZoneName == "" { v.MigrationVlan.ZoneName = "NoTranslationExist" } source := fmt.Sprintf("%s:%d", v.ConfiguredVlan.ZoneName, v.ConfiguredVlan.VlanNumber) dest := fmt.Sprintf("%s:%d", v.MigrationVlan.ZoneName, v.MigrationVlan.VlanNumber) vlanMaps = append(vlanMaps, fmt.Sprintf("%s %*s", source, tableSpace-len(source), dest)) } t.AppendRow(table.Row{strings.Join(vlanMaps, "\n")}) for _, p := range shared.SortPortMaps(data.portMap.PortMaps) { source := portStr(p.Source, data.portMap.SourceDC, data.isRackspace) dest := portStr(p.Migration, data.portMap.MigrationDC, data.isRackspace) portMaps = append(portMaps, fmt.Sprintf("%s %*s", source, tableSpace-len(source), dest)) } if data.portMap.Error != "" { portMaps = append(portMaps, fmt.Sprintf("\nERROR: %s", data.portMap.Error)) } t.AppendRow(table.Row{strings.Join(portMaps, "\n")}) for k, v := range data.audit { errors = append(errors, fmt.Sprintf("%s\n - %s", k, strings.Join(v, "\n - "))) } t.AppendRow(table.Row{strings.Join(errors, "\n\n")}) t.SortBy([]table.SortBy{{Name: fmt.Sprintf("%s Issues", data.model), Mode: table.Dsc}}) t.SetStyle(table.StyleLight) t.Style().Options.SeparateRows = true t.Render() } func csv(results []resultType, fileName, sheetName string) error { file := excelize.NewFile() index := file.NewSheet(sheetName) file.SetCellValue(sheetName, "A1", "ID") file.SetCellValue(sheetName, "B1", "Make Model") file.SetCellValue(sheetName, "C1", "Error") file.SetCellValue(sheetName, "D1", "Source") file.SetCellValue(sheetName, "E1", "Destination") row := 2 for _, result := range results { for _, errs := range result.rawAudit.Data { file.SetCellValue(sheetName, fmt.Sprintf("A%d", row), result.device) file.SetCellValue(sheetName, fmt.Sprintf("B%d", row), result.model) file.SetCellValue(sheetName, fmt.Sprintf("C%d", row), errs.Reason) file.SetCellValue(sheetName, fmt.Sprintf("D%d", row), errs.Current) file.SetCellValue(sheetName, fmt.Sprintf("E%d", row), errs.Destination) row = row + 1 } } file.SetActiveSheet(index) if err := file.SaveAs(fmt.Sprintf("%s.xlsx", fileName)); err != nil { return err } logrus.Infof("csv file written to %s", fmt.Sprintf("%s.xlsx", fileName)) return nil } func validVLANMap(v dcxAutohost.EnvironmentMigrationVlan) bool { if strings.Contains(strings.ToLower(v.ConfiguredVlan.ZoneName), "servicenet") { return false } for _, netSegment := range v.ConfiguredVlan.NetworkSegments { if strings.Contains(strings.ToLower(netSegment), "drac") && v.MigrationVlan.ZoneName == "" { return false } } return true } func portStr(p dcxAutohost.PortMapDetail, dc string, isRackspace bool) string { portNum := func() string { if !isRackspace && p.Name != "" { return p.Name } return strconv.Itoa(p.PortNumber) }() return fmt.Sprintf("%s.%s[%s] (%s)", p.Switch.Number, dc, portNum, p.SwitchPortType) }