// 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 status will upgrade/downgrade CORE device statuses package status import ( "encoding/json" "fmt" "os" "strings" "github.com/360EntSecGroup-Skylar/excelize" "github.com/sirupsen/logrus" "github.rackspace.com/DCM/gomigrate/shared" "github.rackspace.com/gorax/clients/goctk" "github.rackspace.com/gorax/gokeys" ) type ( // Client contains all the data types needed to // initialize and use this package. Client struct { ctk *goctk.Client Statuses []COREStatus } ) // pluginFunc function is executed from the gomigrate caller func pluginFunc(cfg gokeys.Settings) { c := initCfg(cfg) if *argS == "" { logrus.Infof("[GET STATUS] Fetching statuses on %d devices.", len(*argD)) devices := c.ComputerDetails(*argD) for _, r := range devices { fmt.Printf("[%s%d%s] status=%s data_center=%s account=%s platform=%s\n", shared.ClrC, r.Number, shared.ClrN, r.StatusName, r.DataCenter, r.AccountNumber, r.PlatformModel) } if err := deviceStatusXLSX(devices, "device_statuses", "Sheet1"); err != nil { logrus.Error(err) } return } stat, err := c.StatusObj(*argS) if err != nil { logrus.Fatal(err) } logrus.Infof("[UPDATE STATUS] Destination status %d: %s", stat.Number, stat.Name) for _, device := range c.ComputerDetails(*argD) { statuses, reason := c.UpgradePath(device, stat) statusErr := "" if len(statuses) == 0 { fmt.Printf("\033[2K\r[%s%d%s] %sNo upgrade path for device.%s\n", shared.ClrC, device.Number, shared.ClrN, shared.ClrY, shared.ClrN) continue } fmt.Printf("\033[2K\r[%s%d%s] Stepping device through statuses %v; ", shared.ClrM, device.Number, shared.ClrN, statuses) for _, status := range statuses { req := goctk.StatusRequest{NewStatus: status} if reason { req.Reason = "Datacenter Migrations" } if err := c.ctk.SetStatus(device.Number, req); err != nil { // Check for included bandwidth error: // An error occurred when executing method timer with parameters ([], {'new_status': 3}) // for class Computer.Computer, load_value 1085900: Included Bandwidth (GB) // (included_bandwidth) is required. if strings.Contains(err.Error(), "(included_bandwidth) is required") { req.IncludedBandwidth = "1000" if err := c.ctk.SetStatus(device.Number, req); err == nil { continue } } // Sometimes we don't have the Sent to Production step, in this case // ignore the upgrade to Sent to Production and continue to the next status. if req.NewStatus == 63 { continue } // If we failed trying to get the next status, make a last ditch effort to // upgrade to the final status. req = goctk.StatusRequest{NewStatus: stat.Number} if err := c.ctk.SetStatus(device.Number, req); err != nil { statusErr = err.Error() break } } } if statusErr != "" { fmt.Printf("\033[2K\r[%s%d%s] Stepping device through statuses %v; %sError: %s%s\n", shared.ClrC, device.Number, shared.ClrN, statuses, shared.ClrR, statusErr, shared.ClrN) } else { fmt.Printf("\033[2K\r[%s%d%s] Stepping device through statuses %v; %sDone.%s\n", shared.ClrC, device.Number, shared.ClrN, statuses, shared.ClrG, shared.ClrN) } } } // UpgradePath is where the logic will live to determine the upgrade steps to step the device // through to get to the final status. func (c *Client) UpgradePath(computer goctk.ComputerLight, stat COREStatus) ([]int, bool) { r := make(Path, 0) // If this is a single step upgrade/downgrade... for _, upStatus := range computer.UpgradeStates { if strings.EqualFold(upStatus, stat.Name) { return []int{stat.Number}, true } } for _, downStatus := range computer.DowngradeStates { if strings.EqualFold(downStatus, stat.Name) { return []int{stat.Number}, true } } // Special handling of the upgrade path if the Deployment SKU exist // on the device. if HasSKU(computer, 111507) { for _, path := range []Path{PathDeploymentOC, PathDeploymentSM} { if path.ValidPath(computer.StatusCode, stat.Number) { return path.Steps(computer.StatusCode, stat.Number), false } } } // Take the PathStd approach if the source and destination statuses are both in the PathStd slice. for _, path := range OrderedPaths { if path.ValidPath(computer.StatusCode, stat.Number) { return path.Steps(computer.StatusCode, stat.Number), false } } return r, false } // ComputerDetails will chunk and pull a list of current device details func (c *Client) ComputerDetails(s []int) map[int]goctk.ComputerLight { r := make(map[int]goctk.ComputerLight) chunks := make([][]int, 0) for i := 0; i < len(s); i += 250 { end := i + 250 if end > len(s) { end = len(s) } chunks = append(chunks, s[i:end]) } for _, sGroup := range chunks { result, err := c.ctk.ComputerQueryLight(sGroup) if err != nil { logrus.Errorf("status:ComputerDetails:%s", err) continue } for _, item := range result.Result { r[item.Number] = *item } } return r } // ComputerStatusResp is the return type from the ComputerStatuses CTK call type ComputerStatusResp []struct { Count int `json:"count"` LoadArg interface{} `json:"load_arg"` Limit int `json:"limit"` Result []COREStatus `json:"result"` Offset int `json:"offset"` Class string `json:"class"` } // COREStatus is the status name and number from CTK type COREStatus struct { Name string `json:"name"` Number int `json:"number"` } // ComputerStatuses will retrieve a list of valid statuses from CORE func (c *Client) ComputerStatuses() ([]COREStatus, error) { query := []byte(`{"attributes":["name","number"],"class":"Computer.Status","load_arg":{"active":true},"load_method":"loadList"}`) byt, err := c.ctk.Query(query) if err != nil { return nil, fmt.Errorf("computerStatuses:ctk.Query:%s", err) } var payload ComputerStatusResp if err = json.Unmarshal(byt, &payload); err != nil { return nil, fmt.Errorf("computerStatuses:json.Unmarshal:%s", err) } if len(payload) == 0 { return nil, fmt.Errorf("computerStatuses:0 CORE statuses found:%v", byt) } return payload[0].Result, nil } // StatusObj will return the CORE status ID for the status name // passed in. func (c *Client) StatusObj(s string) (COREStatus, error) { if len(c.Statuses) == 0 { statuses, err := c.ComputerStatuses() if err != nil { return COREStatus{}, fmt.Errorf("statusObj:%s", err) } c.Statuses = statuses } for _, status := range c.Statuses { if strings.EqualFold(status.Name, s) { return status, nil } } return COREStatus{}, fmt.Errorf("statusObj:status not found for %s", s) } // HasSKU will check for the given SKU on a CORE computer. func HasSKU(computer goctk.ComputerLight, sku int) bool { for i := range computer.SKUs { if computer.SKUs[i] == sku { return true } } return false } func deviceStatusXLSX(results map[int]goctk.ComputerLight, fileName, sheetName string) error { var ( file = excelize.NewFile() index = file.NewSheet(sheetName) row = 2 ) file.SetCellValue(sheetName, "A1", "Device ID") file.SetCellValue(sheetName, "B1", "Account Number") file.SetCellValue(sheetName, "C1", "Platform Model") file.SetCellValue(sheetName, "D1", "Hostname") file.SetCellValue(sheetName, "E1", "Primary IP") file.SetCellValue(sheetName, "F1", "Status") file.SetCellValue(sheetName, "G1", "DataCenter") for _, r := range results { file.SetCellValue(sheetName, fmt.Sprintf("A%d", row), r.Number) file.SetCellValue(sheetName, fmt.Sprintf("B%d", row), r.AccountNumber) file.SetCellValue(sheetName, fmt.Sprintf("C%d", row), r.PlatformModel) file.SetCellValue(sheetName, fmt.Sprintf("D%d", row), r.Name) file.SetCellValue(sheetName, fmt.Sprintf("E%d", row), r.PrimaryIP) file.SetCellValue(sheetName, fmt.Sprintf("F%d", row), r.StatusName) file.SetCellValue(sheetName, fmt.Sprintf("G%d", row), r.DataCenter) row = row + 1 } file.SetActiveSheet(index) if err := file.SaveAs(fmt.Sprintf("%s.xlsx", fileName)); err != nil { return err } if path, err := os.Getwd(); err == nil { fmt.Printf("XLSX file written to %s/%s\n", path, fmt.Sprintf("%s.xlsx", fileName)) return nil } fmt.Printf("XLSX file written to %s\n", fmt.Sprintf("%s.xlsx", fileName)) return 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) } ctk, err := goctk.NewCTK(goctk.Token(iKey.Password), goctk.WithStaging(false)) if err != nil { logrus.Fatal(err) } return &Client{ctk: ctk, Statuses: make([]COREStatus, 0)} }