// 2019 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. // To build this app, run the following command: // CGO_ENABLED=0 GOARCH=amd64 go build -a -installsuffix cgo -o bin/change_dc change_dc.go package post import ( "context" "encoding/json" "fmt" "log" "net/http" "net/url" "os" "strconv" "time" "github.com/360EntSecGroup-Skylar/excelize" "github.com/jedib0t/go-pretty/v6/table" "github.com/jokelyo/argparse" "github.com/pkg/errors" "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/gokeys" ) const ( timeout = 600 * time.Second ) type resultType struct { result tools.Device } type result struct { Audits map[string][]string `json:"audits"` Error map[string][]string `json:"errors"` } var ( argMoveGroups *[]string argDevices *[]int argAccounts *[]string argIP *string age *int routines *int csvFlag *bool ) // 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 find NetDevices HA configuration cmd := p.NewCommand("post", "Runs the post-migration audits on a set of devices or move-groups") argMoveGroups = shared.ArgMoveGroups(cmd) argDevices = shared.ArgDevices(cmd) argAccounts = shared.ArgAccounts(cmd) argIP = cmd.String("", "ip", &argparse.Options{Help: "override management-ip address"}) age = shared.ArgUnipedeAge(cmd, 600) csvFlag = shared.ArgFlag(cmd, "csv", "Create a CSV results file") routines = shared.ArgRoutines(cmd, 5) return core.Plugin{CMD: cmd, Func: pluginFunc} } // pluginFunc function will prompt for a list of move-groups, then query the migration database // for a listing of network devices to run the failover_device call on. // It should then determine which devices in-fact have an HA configuration and return the // HA status func pluginFunc(cfg gokeys.Settings) { var ( ip string collection string devices []tools.Device ) if argIP != nil { ip = *argIP } // Pull a core token from the users gokeys keychain coreKey, err := cfg.Core(false) if err != nil { log.Fatalf("gokeys.Core:%s", err) } identKey, err := cfg.Identity(false) if err != nil { log.Fatalf("gokeys.Identity:%s", err) } if argMoveGroups == nil && argDevices == nil { log.Fatal("must specify a move-group or device") } if len(*argMoveGroups) > 0 { collection = shared.GetCollection((*argMoveGroups)[0]) devices, err = shared.MongoDevices(*argMoveGroups, nil, nil, collection, coreKey.Password, nil) } else if len(*argAccounts) > 0 { accounts := make([]int, 0) for _, a := range *argAccounts { account, err := strconv.Atoi(a) if err != nil { continue } accounts = append(accounts, account) } collection = shared.GetAccountCollection((*argAccounts)[0], coreKey.Password) devices, err = shared.MongoDevices(nil, accounts, nil, collection, coreKey.Password, nil) } else if len(*argDevices) > 0 { collection = shared.GetDeviceCollection((*argDevices)[0], coreKey.Password) devices, err = shared.MongoDevices(nil, nil, *argDevices, collection, coreKey.Password, nil) } if err != nil { logrus.Fatal(err) } if len(devices) == 0 { logrus.Warningf("no devices found in collection %s", collection) return } logrus.Infof("Running post-migration audit on %d devices. Running %d processes concurrently.", len(devices), *routines) // Creating a buffered channel that will block when we have reached the // passed in limit for concurrency semChan := make(chan struct{}, *routines) // Create a non-blocking channel that will receive all the responses // back from validate. resChan := make(chan *resultType) // Close the channels when this function has exited. defer func() { close(semChan) close(resChan) }() for _, device := range devices { go func(device tools.Device) { // 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{}{} res := getPostAudit(device, collection, identKey.Password, ip, *age) // now we can send the result struct through the resultsChan resChan <- &res // Now that we're done, pull an empty struct from the channel, // freeing up one spot for another routine to take. <-semChan }(device) } // 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++ { result := <-resChan results = append(results, *result) pPrint(*result) } if *csvFlag { if err := csv(results, "Post_Migration_Audit", "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{data.ID, fmt.Sprintf("%s Issues", data.Device.Device.ComponentConfigType)}) hasError := false for k, v := range data.Error { if len(v) == 0 { t.AppendRow(table.Row{k, ""}) } for _, i := range v { hasError = true t.AppendRow(table.Row{k, i}) } } for k, v := range data.Audits { if len(v) == 0 { t.AppendRow(table.Row{k, ""}) } for _, i := range v { hasError = true t.AppendRow(table.Row{k, i}) } } if !hasError { logrus.Infof("%d: no errors found", data.ID) return } t.SortBy([]table.SortBy{{Name: fmt.Sprintf("%s Issues", data.Device.Device.ComponentConfigType), Mode: table.Dsc}}) t.SetStyle(table.StyleDefault) t.Render() } func csv(results []resultType, fileName, sheetName string) error { var ( file = excelize.NewFile() index = file.NewSheet(sheetName) row = 2 ) file.SetCellValue(sheetName, "A1", "Device ID") file.SetCellValue(sheetName, "B1", "Device Type") file.SetCellValue(sheetName, "C1", "Audit Type") file.SetCellValue(sheetName, "D1", "Audit Result") for _, r := range results { for k, v := range r.Error { for _, i := range v { file.SetCellValue(sheetName, fmt.Sprintf("A%d", row), r.ID) file.SetCellValue(sheetName, fmt.Sprintf("B%d", row), r.Device.Device.ComponentConfigType) file.SetCellValue(sheetName, fmt.Sprintf("C%d", row), k) file.SetCellValue(sheetName, fmt.Sprintf("D%d", row), i) row = row + 1 } } for k, v := range r.Audits { for _, i := range v { file.SetCellValue(sheetName, fmt.Sprintf("A%d", row), r.ID) file.SetCellValue(sheetName, fmt.Sprintf("B%d", row), r.Device.Device.ComponentConfigType) file.SetCellValue(sheetName, fmt.Sprintf("C%d", row), k) file.SetCellValue(sheetName, fmt.Sprintf("D%d", row), i) 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 } // getPostAudit function will take a device and reach out to the NSI service that determines // the correct failover device by logging into the device and looking for certain criteria. func getPostAudit(d tools.Device, collection, token, ip string, age int) resultType { var ( headers = map[string]string{ "Accept": "application/json", "Connection": "close", "X-Auth-Token": token, } result = resultType{result{}, d} fullURL = fmt.Sprintf("%s/audit/post_migration/%s/device/%d", shared.MigAPI, collection, d.ID) ) ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() // Build the request req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil) if err != nil { //logrus.Errorf("getPostAudit:NewRequest:%d:%s", d.ID, err) result.Error = map[string][]string{"error": {err.Error()}} return result } par := url.Values{ "age": []string{strconv.Itoa(age)}, } if ip != "" { logrus.Infof("overriding management-ip to use %s", ip) par["address"] = []string{ip} } req.URL.RawQuery = par.Encode() for key, value := range headers { req.Header.Set(key, value) } // Send the request via a client // Do sends an HTTP request and // returns an HTTP response r, err := http.DefaultClient.Do(req) if err != nil { //logrus.Errorf("getPostAudit:Do:%d:%s", d.ID, err) result.Error = map[string][]string{"error": {err.Error()}} return result } if r != nil { defer func() { if defErr := r.Body.Close(); defErr != nil { err = errors.Wrap(defErr, "defer error") } }() } if r.StatusCode > http.StatusNoContent { //logrus.Errorf("resp_err:%d:%s", d.ID, r.Status) result.Error = map[string][]string{"error": {err.Error()}} return result } // Use json.Decode for reading streams of JSON data if err := json.NewDecoder(r.Body).Decode(&result.result); err != nil { //logrus.Errorf("getPostAudit:Decode:%d:%s", d.ID, err) result.Error = map[string][]string{"error": {err.Error()}} return result } return result }