// 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 finalize import ( "fmt" "log" "os" "strconv" "strings" "time" "github.com/360EntSecGroup-Skylar/excelize" "github.com/jedib0t/go-pretty/v6/table" "github.com/sirupsen/logrus" "github.rackspace.com/DCM/gomigrate/shared" migrationMongo "github.rackspace.com/DCM/migration/pkg/collection/mongo" "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 { id int model string moveGroup string elapsedTime string finalize dcxAutohost.JobStatus postFinalize migrationAPI.PostFinalizeResp } client struct { dcx *dcxAutohost.Client arg arg user string coreToken string } arg struct { csv bool devices []int moveGroups []string postFinalize bool routines int } ) // pluginFunc function is executed from the gomigrate caller func pluginFunc(cfg gokeys.Settings) { // Set all the key from the arg parser c := client{ arg: arg{ csv: *args[argC].(*bool), devices: *args[argD].(*[]int), moveGroups: *args[argM].(*[]string), postFinalize: *args[argP].(*bool), routines: *args[argR].(*int), }, } // Pull an identity token from the users gokeys keychain key, err := cfg.Identity(false) if err != nil { log.Fatal(err) } // Pull a core token from the gokeys keychain coreKey, err := cfg.Core(false) if err != nil { log.Fatal(err) } c.coreToken = coreKey.Password // Pull SSO creds from the gokeys keychain sso, err := cfg.SSO() if err != nil { log.Fatal(err) } c.user = sso.Username // Get the device list from a list of move-groups or devices. devices, err := shared.MongoDevices(c.arg.moveGroups, nil, c.arg.devices, "", 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)", c.arg.moveGroups, c.arg.devices) } // Creating a buffered channel that will block when we have reached the // passed in limit for concurrency semChan := make(chan struct{}, c.arg.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) }() // Grab the client for the DCX API c.dcx, err = dcxAutohost.New(dcxAutohost.WithCredential(sso.Username, sso.Password), false) if err != nil { logrus.Fatal(err) } if c.arg.postFinalize { // Grab the client for the Migration API m := migrationAPI.New(key.Password, false) logrus.Infof("Running post-finalize on %d devices", len(devices)) for _, device := range shared.SortDevices(devices) { logrus.Infof("Running post-finalize on %d ...", device.ID) resp := resultType{ id: device.ID, model: device.Device.MakeModel, elapsedTime: "", finalize: dcxAutohost.JobStatus{}, postFinalize: migrationAPI.PostFinalizeResp{}, } var err error start := time.Now() resp.postFinalize, err = m.PostFinalize(device.MoveGroup, strconv.Itoa(device.ID)) if err != nil { logrus.Errorf("%d ERROR: %s", device.ID, err) } resp.elapsedTime = time.Since(start).String() pPrint(resp) } return } logrus.Infof("Running finalize on %d devices. Running %d processes concurrently.", len(devices), c.arg.routines) for _, device := range shared.SortDevices(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 := c.runPortPrep(device, cfg) res.id = device.ID res.model = device.Device.Type res.moveGroup = device.MoveGroup // post results to status endpoint logrus.Debugf("Recording %d result to DB ...", device.ID) // Pull an identity token from the users gokeys keychain immediately before // the status call to try and prevent race condition of token expiring key, err := cfg.Identity(false) if err != nil { log.Fatal(err) } c.statusUpdate(res, migrationAPI.New(key.Password, false)) // 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 the csv flag is set, call the csv() function to write // the results to a file. if c.arg.csv { if err := csv(results, "FinalizeResults", "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.id, data.model), "Elapsed Time"}) finalize := "" if len(data.finalize.Errors) > 0 || data.finalize.Message != nil { finalize = fmt.Sprintf("\nDCXFinalize:%s:\n", data.finalize.Name) if len(data.finalize.Errors) != 0 { for _, c := range data.finalize.Errors { finalize += fmt.Sprintf(" - ERROR: %s\n", c) } } if data.finalize.Message != nil { finalize += fmt.Sprintf(" - CHANGE: %s", data.finalize.Message) } } postFinalize := "" if len(data.postFinalize.Data.Errors) != 0 || len(data.postFinalize.Data.Changes) != 0 { postFinalize = "\nPostFinalize:Results:\n" if len(data.postFinalize.Data.Errors) != 0 { for _, c := range data.postFinalize.Data.Errors { postFinalize += fmt.Sprintf(" - ERROR: %s\n", c) } } if len(data.postFinalize.Data.Changes) != 0 { for _, c := range data.postFinalize.Data.Changes { postFinalize += fmt.Sprintf(" - CHANGE: %s\n", c) } } } message := fmt.Sprintf("%s\n%s", finalize, postFinalize) if finalize == "" { message = postFinalize } t.AppendRow(table.Row{message, data.elapsedTime}) 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", "Finalize Status") file.SetCellValue(sheetName, "D1", "Finalize Percent") file.SetCellValue(sheetName, "E1", "Finalize Message") file.SetCellValue(sheetName, "F1", "Finalize Name") file.SetCellValue(sheetName, "G1", "Post Finalize Changes") file.SetCellValue(sheetName, "H1", "Post Finalize Errors") file.SetCellValue(sheetName, "I1", "Elapsed Time") row := 2 for _, result := range results { file.SetCellValue(sheetName, fmt.Sprintf("A%d", row), result.id) file.SetCellValue(sheetName, fmt.Sprintf("B%d", row), result.model) file.SetCellValue(sheetName, fmt.Sprintf("C%d", row), result.finalize.Status) file.SetCellValue(sheetName, fmt.Sprintf("D%d", row), result.finalize.Pct) file.SetCellValue(sheetName, fmt.Sprintf("E%d", row), string(result.finalize.Message)) file.SetCellValue(sheetName, fmt.Sprintf("F%d", row), result.finalize.Name) file.SetCellValue(sheetName, fmt.Sprintf("G%d", row), strings.Join(result.postFinalize.Data.Changes, "\n")) file.SetCellValue(sheetName, fmt.Sprintf("H%d", row), strings.Join(result.postFinalize.Data.Errors, "\n")) file.SetCellValue(sheetName, fmt.Sprintf("I%d", row), result.elapsedTime) 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 (c *client) runPortPrep(d tools.Device, cfg gokeys.Settings) resultType { var ( resp resultType timeout = time.After(time.Minute * 20) interval = time.NewTicker(time.Second * 10) // Record the start time of the executing request start = time.Now() errs = 0 ) var jobID string var err error if d.Linked.ID == 0 { jobID, err = c.dcx.FinalizeReservation(strconv.Itoa(d.ID)) } else { jobID, err = c.dcx.FinalizeCurrent(strconv.Itoa(d.ID)) } if err != nil { resp.finalize.Errors = append(resp.finalize.Errors, err.Error()) } else { logrus.Debugf("Finalize job %s for %s", jobID, d.ID) Poller: for { select { case <-timeout: resp.elapsedTime = time.Since(start).String() resp.finalize.Errors = append(resp.finalize.Errors, fmt.Sprintf("ERROR:finalize timeout for %d", d.ID)) break Poller case <-interval.C: resp.finalize, err = c.dcx.Job(jobID) if err != nil { errs++ logrus.Debugf("ERROR:%s:JobPolling:%s", d.ID, err) if errs > 3 { resp.finalize.Errors = append(resp.finalize.Errors, err.Error()) resp.elapsedTime = time.Since(start).String() break Poller } } if len(resp.finalize.Errors) > 0 { resp.elapsedTime = time.Since(start).String() break Poller } if resp.finalize.Pct == 100 { break Poller } } } } // Pull an identity token from the users gokeys keychain immediately before // the call to try and prevent race condition of token expiring key, err := cfg.Identity(false) if err != nil { resp.finalize.Errors = append(resp.finalize.Errors, "Unable to run post-finalize: "+err.Error()) return resp } resp.postFinalize, err = migrationAPI.New(key.Password, false).PostFinalize(d.MoveGroup, strconv.Itoa(d.ID)) resp.elapsedTime = time.Since(start).String() if err != nil { resp.finalize.Errors = append(resp.finalize.Errors, "Post-finalize error: "+err.Error()) } return resp } func (c *client) statusUpdate(result resultType, m *migrationAPI.Client) { collection := shared.GetCollection(result.moveGroup) if collection == "" { logrus.Errorf("%d: unable to find collection from MG, not recording status update", result.id) return } payload := make([]migrationMongo.StatusFinalize, 0) payload = append(payload, migrationMongo.StatusFinalize{ ID: result.id, Username: c.user, Timestamp: time.Now().Unix(), JobStatus: result.finalize, }) err := m.StatusUpdateFinalize(collection, result.moveGroup, payload) if err != nil { logrus.Errorf("error updating status results for %d: %s", result.id, err.Error()) } }