// 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. package notmoving import ( "errors" "fmt" "os" "strings" "github.rackspace.com/gorax/clients/migrationAPI" "github.com/jedib0t/go-pretty/text" "github.rackspace.com/DCM/tools" "github.com/jedib0t/go-pretty/table" "github.com/jokelyo/argparse" "github.com/sirupsen/logrus" "github.rackspace.com/DCM/gomigrate/core" "github.rackspace.com/DCM/gomigrate/shared" "github.rackspace.com/gorax/gokeys" migrationMongo "github.rackspace.com/DCM/migration/pkg/collection/mongo" ) type ( deviceInfo struct { Device tools.Device CurrentMG string NeededMG string } ) var ( argMG *string argYes *bool ) // Plugin function will return a argparse.Command type back to the parent parser // nolint:typecheck func Plugin(p *core.Parser) core.Plugin { cmd := p.NewCommand("not_moving", "Updates MG assignments for not-moving groups") argMG = cmd.String("m", "mg", &argparse.Options{Help: "set move group", Required: true}) argYes = cmd.Flag("y", "yes", &argparse.Options{Help: "auto-confirm changes"}) return core.Plugin{CMD: cmd, Func: pluginFunc} } // pluginFunc function will prompt for a destination DC by name, then a list of CORE devices to change the DC on. // Once provided, we will iterate through the list of devices and update the DC using CTK. func pluginFunc(cfg gokeys.Settings) { // Pull a core token from the users gokeys keychain key, err := cfg.Core(false) if err != nil { logrus.Fatal(err) } // Pull an identity token from the users gokeys keychain iKey, err := cfg.Identity(false) if err != nil { logrus.Fatal(err) } mgMetadata, err := shared.GetMetadata(*argMG, key.Password) if err != nil { logrus.Fatal(err) } if mgMetadata.FullDC { logrus.Fatal(errors.New("migration defined as Full DC migration, exiting")) } // Create a new Migration API Client migapi := migrationAPI.New(iKey.Password, false) nmGroup := strings.Replace(*argMG, "-M", "-N", 1) nmMetadata, err := shared.GetMetadata(nmGroup, key.Password) if err != nil { if !strings.Contains(err.Error(), "unable to find metadata for") { logrus.Fatal(err) } } if nmMetadata.ID == "" { if *argYes || shared.ConfirmPrompt(fmt.Sprintf("nm group [%s] does not exist. Create?", nmGroup)) { payload := migrationMongo.CreateNMGroupPayload{ MovingGroupName: mgMetadata.ID, NotMovingGroupName: nmGroup, } resp, err := migapi.NotMovingGroupCreate(payload) if err != nil { errMsg := err.Error() if resp != nil && resp.Error != nil { errMsg = errMsg + " " + *resp.Error } logrus.Fatal(errMsg) } } } logrus.Infof("Retrieving devices for MG [%s] ...", *argMG) mgDevices, err := shared.MongoDevices([]string{*argMG}, nil, nil, mgMetadata.Collection, key.Password, make(map[string]interface{})) if err != nil { logrus.Fatal(err) } accounts := make([]int, 0) for _, d := range mgDevices { if accountMatch(accounts, d.Account.Number) { continue } accounts = append(accounts, d.Account.Number) } devicesByEnv := make(map[string][]deviceInfo) for _, d := range mgDevices { if _, ok := devicesByEnv[d.Environment.Number]; !ok { devicesByEnv[d.Environment.Number] = make([]deviceInfo, 0) } } devices := make([]tools.Device, 0) logrus.Infof("Retrieving all devices for accounts in MG [%s] ...", *argMG) // iterating per account to avoid dbapi bug due to data size for _, a := range accounts { d, err := shared.MongoDevices(nil, []int{a}, nil, mgMetadata.Collection, key.Password, make(map[string]interface{})) if err != nil { logrus.Fatal(err) } devices = append(devices, d...) } logrus.Infof("Retrieving filters for migration [%s] ...", mgMetadata.Collection) filters, err := shared.MongoFilters(mgMetadata.FiltersCollection, key.Password) if err != nil { logrus.Fatal(err) } // scan for hyps in MG hypsMoving := make(map[string]bool) for _, d := range devices { if strings.Contains(strings.ToLower(d.Device.MakeModel), "hypervisor") && d.Environment.Number != "" { if moving, ok := hypsMoving[d.Environment.Number]; ok && moving { continue } hypsMoving[d.Environment.Number] = false if filterMatch(filters, d.Environment.Current.Container) { hypsMoving[d.Environment.Number] = true } } } // scan all devices per env and set current/needed MGs changeCount := 0 for env := range devicesByEnv { for _, d := range devices { if d.Environment.Number == env { neededMG := neededMG(d, filters, *argMG, nmGroup, hypsMoving[env]) if d.MoveGroup != neededMG { changeCount++ } devicesByEnv[env] = append(devicesByEnv[env], deviceInfo{ Device: d, CurrentMG: d.MoveGroup, NeededMG: neededMG, }) } } } prettyPrint(devicesByEnv) if changeCount > 0 { if *argYes || shared.ConfirmPrompt("Update move-group for device(s)?") { for _, devices := range devicesByEnv { for _, d := range devices { if d.CurrentMG != d.NeededMG { err = updateNMGroup(migapi, d) if err != nil { logrus.Errorf("%d: %s", d.Device.ID, err) } } } } logrus.Info("updates completed") } } else { logrus.Info("No changes needed") } } func updateNMGroup(c *migrationAPI.Client, d deviceInfo) error { payload := migrationMongo.UpdateNMGroupPayload{ NotMovingGroupName: d.NeededMG, DeviceID: d.Device.ID, } resp, err := c.NotMovingGroupUpdate(payload) errMsg := "" if err != nil { errMsg = err.Error() if resp != nil && resp.Error != nil { errMsg = errMsg + " " + *resp.Error } return errors.New(errMsg) } return nil } func accountMatch(accounts []int, account int) bool { for _, a := range accounts { if a == account { return true } } return false } func filterMatch(filters []shared.MigrationFilter, container string) bool { for _, f := range filters { if strings.EqualFold(f.Type, "cabinet") && strings.EqualFold(f.Value, container) { return true } } return false } func neededMG(device tools.Device, filters []shared.MigrationFilter, mg, nmGroup string, hypsMoving bool) string { // for virtual devices, make decision based on hyps if strings.Contains(strings.ToLower(device.Device.MakeModel), "virtual") { if hypsMoving { return mg } return nmGroup } if filterMatch(filters, device.Environment.Current.Container) { return mg } return nmGroup } // prettyPrint func will take a structured type of response data and render a table // output to stdout. func prettyPrint(devicesByEnv map[string][]deviceInfo) { //var r = strings.NewReplacer( // fmt.Sprintf("%s:public:", migrDC), "", // fmt.Sprintf("%s:publicnet:", migrDC), "", // "zone", "aggr", // "omega", "internal", //) t := table.NewWriter() t.SetOutputMirror(os.Stdout) t.AppendHeader(table.Row{"Device", "Account", "Env", "Cab", "Model", "Current MG", "Needed MG"}) t.SetColumnConfigs([]table.ColumnConfig{ { Name: "Model", WidthMax: 30, }, }) for env, devices := range devicesByEnv { for _, d := range devices { row := table.Row{ d.Device.ID, d.Device.Account.Number, env, d.Device.Environment.Current.Container, d.Device.Device.MakeModel, d.CurrentMG, d.NeededMG, } if d.CurrentMG != d.NeededMG { row = table.Row{ d.Device.ID, d.Device.Account.Number, env, d.Device.Environment.Current.Container, d.Device.Device.MakeModel, text.FgYellow.Sprint(d.CurrentMG), text.FgYellow.Sprint(d.NeededMG), } } t.AppendRow(row) } } t.SortBy([]table.SortBy{{Name: "Account", Mode: table.Asc}, {Name: "Device", Mode: table.Asc}}) t.SetStyle(table.StyleDefault) t.Render() }