// 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 shared import ( "bufio" "bytes" "encoding/json" "fmt" "log" "math" "os" "regexp" "sort" "strconv" "strings" "github.rackspace.com/DCM/tools/migMeta" "go.mongodb.org/mongo-driver/bson/primitive" "github.com/google/go-cmp/cmp" "github.com/jedib0t/go-pretty/v6/table" "github.com/pkg/errors" "github.com/sirupsen/logrus" input "github.com/tcnksm/go-input" "github.rackspace.com/DCM/migui/pkg/notes" "github.rackspace.com/DCM/tools" "github.rackspace.com/gorax/clients/dcxAutohost" "github.rackspace.com/gorax/clients/migrationAPI" "github.rackspace.com/gorax/clients/mongodbAPI" "github.rackspace.com/gorax/clients/mydp" "github.rackspace.com/gorax/clients/passwordsafe" ) const ( // Color codes to highlight terminal text // Clr0 terminal color Black Clr0 = "\x1b[30;1m" // ClrR terminal color Red ClrR = "\x1b[31;1m" // ClrG terminal color Green ClrG = "\x1b[32;1m" // ClrY terminal color Yellow ClrY = "\x1b[33;1m" // ClrB terminal color Blue ClrB = "\x1b[34;1m" // ClrM terminal color Magenta ClrM = "\x1b[35;1m" // ClrC terminal color Cyan ClrC = "\x1b[36;1m" // ClrW terminal color White ClrW = "\x1b[37;1m" // ClrN will end the color text ClrN = "\x1b[0m" // MigAPI is the api backend for most of the gomigrate functions MigAPI = "https://migration.nsi.rackspace.com" // ZSPAPI is the service API used to interface with ZSP ZSPAPI = "https://service.nsi.rackspace.com/acs/zsp/auth" ) type ( // MigrationFilter defines the filter structure in the DB for a migration MigrationFilter struct { ID primitive.ObjectID `bson:"_id" json:"_id"` Value string `bson:"value" json:"value"` Type string `bson:"type" json:"type"` } ) var ( collectionRE = regexp.MustCompile(`^(\S+)_\d+$`) // Example: IAD2-01-M04 moveGroupRE = regexp.MustCompile(`(?i)(\S+)-(\S+)-(\S+)`) // Example: BD-301A-1-20-501A-9-20 bridgeGroupRE = regexp.MustCompile(`BD-((\d+A-\d+)|(\d+A))-(\d+)-((\d+A-\d+)|(\d+A))-(\d+)`) // UI defines basic configuration for the ui ask pkg UI = &input.UI{Writer: os.Stdout, Reader: os.Stdin} // UIOpts defines basic options for the ui ask pkg UIOpts = &input.Options{Required: true, HideOrder: true} ) // Truncate shortens a string if the length of the string exceeds the // number passed in. func Truncate(str string, max int) string { bnoden := str if len(str) > max { if max > 3 { max -= 3 } bnoden = str[0:max] + "..." } return bnoden } // GetCollection is a simple helper function to parse and return // the DC (collection) name from a move-group string // Typically in format: IAD2-MG-01 func GetCollection(moveGroup string) string { mgMatch := moveGroupRE.FindStringSubmatch(moveGroup) // If we match example: IAD2-01-M04 if len(mgMatch) == 4 { return strings.ToLower(fmt.Sprintf("%s_%s", mgMatch[1], mgMatch[2])) } return "" } // GetMetadata is a simple helper function to parse and return metadata about a move-group func GetMetadata(moveGroup, token string) (tools.Metadata, error) { client := mongodbAPI.New(mongodbAPI.WithToken(token), "migration", false) byt, err := client.Find(&mongodbAPI.FindQuery{ Query: map[string]string{"_id": moveGroup}}, tools.MetaCollection) if err != nil { logrus.Fatal(err) } var payload struct { Result []tools.Metadata `json:"result"` } // Use json.Decode for reading streams of JSON data if err := json.NewDecoder(bytes.NewReader(byt)).Decode(&payload); err != nil { logrus.Fatalf("gomigrate:GetMetadata:Decode:%s:%s", moveGroup, err) } if len(payload.Result) == 0 { return tools.Metadata{}, fmt.Errorf("unable to find metadata for %s", moveGroup) } return payload.Result[0], nil } // GetMetadataFromCollection is a simple helper function to parse and return metadata about a collection func GetMetadataFromCollection(collection, token string) (migMeta.Migration, error) { client := mongodbAPI.New(mongodbAPI.WithToken(token), "migration", false) byt, err := client.Find(&mongodbAPI.FindQuery{ Query: map[string]string{"_id": collection}}, migMeta.Collection) if err != nil { logrus.Fatal(err) } var payload struct { Result []migMeta.Migration `json:"result"` } // Use json.Decode for reading streams of JSON data if err := json.NewDecoder(bytes.NewReader(byt)).Decode(&payload); err != nil { logrus.Fatalf("gomigrate:GetMetadataFromCollection:Decode:%s:%s", collection, err) } if len(payload.Result) == 0 { return migMeta.Migration{}, fmt.Errorf("unable to find metadata for %s", collection) } return payload.Result[0], nil } // GetDeviceCollection is a simple helper function to parse and return a device collection func GetDeviceCollection(deviceID int, token string) string { client := mongodbAPI.New(mongodbAPI.WithToken(token), "migration", false) byt, err := client.Find(&mongodbAPI.FindQuery{ Query: map[string]interface{}{"devices": deviceID, "active": true}}, tools.MetaCollection, ) if err != nil { logrus.Fatal(err) } var payload struct { Result []tools.Metadata `json:"result"` } // Use json.Decode for reading streams of JSON data if err := json.NewDecoder(bytes.NewReader(byt)).Decode(&payload); err != nil { logrus.Fatalf("gomigrate:GetDeviceCollection:Decode:%d:%s", deviceID, err) } length := len(payload.Result) switch { case length == 0: logrus.Warningf("unable to find collection for %d", deviceID) resp, err := UI.Ask("Enter collection name", UIOpts) if err != nil { log.Fatal(err) } return strings.ToLower(resp) case length == 1: logrus.Debugf("using %s collection for device %d", payload.Result[0].Collection, deviceID) return payload.Result[0].Collection case isSameCollection(payload.Result): logrus.Debugf("using %s collection for device %d", payload.Result[0].Collection, deviceID) return payload.Result[0].Collection default: logrus.Infof("Device %d in multiple active move-groups:", deviceID) printOpts(payload.Result) var selected tools.Metadata for { resp, err := UI.Ask("Enter collection #", UIOpts) if err != nil { logrus.Error(err) continue } i, err := strconv.Atoi(resp) if err != nil { logrus.Error(err) continue } if len(payload.Result) < i || i < 1 { logrus.Error("select a valid number") continue } selected = payload.Result[i-1] break } return selected.Collection } } func isSameCollection(m []tools.Metadata) bool { for i := 1; i < len(m); i++ { if m[i].Collection != m[0].Collection { return false } } return true } // GetAccountMetadata is a simple helper function to parse and return an account number and MG func GetAccountMetadata(account int, token string) (string, string) { client := mongodbAPI.New(mongodbAPI.WithToken(token), "migration", false) byt, err := client.Find(&mongodbAPI.FindQuery{ Query: map[string]int{"accounts": account}}, tools.MetaCollection) if err != nil { logrus.Fatal(err) } var payload struct { Result []tools.Metadata `json:"result"` } // Use json.Decode for reading streams of JSON data if err := json.NewDecoder(bytes.NewReader(byt)).Decode(&payload); err != nil { logrus.Fatalf("gomigrate:GetAccountMetadata:Decode:%s:%s", account, err) } length := len(payload.Result) switch { case length == 0: logrus.Warningf("unable to find collection for account %d", account) resp, err := UI.Ask("Enter collection name", UIOpts) if err != nil { log.Fatal(err) } resp2, err := UI.Ask("Enter move group name", UIOpts) if err != nil { log.Fatal(err) } return strings.ToLower(resp), strings.ToUpper(resp2) case length == 1: logrus.Debugf("using %s collection for account %d", payload.Result[0].Collection, account) return payload.Result[0].Collection, payload.Result[0].ID default: logrus.Info("Account in multiple active move-groups:") printOpts(payload.Result) var selected tools.Metadata for { resp, err := UI.Ask("Enter collection #", UIOpts) if err != nil { logrus.Error(err) continue } i, err := strconv.Atoi(resp) if err != nil { logrus.Error(err) continue } if len(payload.Result) < i || i < 1 { logrus.Error("select a valid number") continue } selected = payload.Result[i-1] break } return selected.Collection, selected.ID } } // GetEnvMetadata is a simple helper function to parse and return an env collection and MG func GetEnvMetadata(envNumber, token string) (string, string) { client := mongodbAPI.New(mongodbAPI.WithToken(token), "migration", false) byt, err := client.Find(&mongodbAPI.FindQuery{ Query: map[string]string{"environments": envNumber}}, tools.MetaCollection) if err != nil { logrus.Fatal(err) } var payload struct { Result []tools.Metadata `json:"result"` } // Use json.Decode for reading streams of JSON data if err := json.NewDecoder(bytes.NewReader(byt)).Decode(&payload); err != nil { logrus.Fatalf("gomigrate:GetEnvMetadata:Decode:%s:%s", envNumber, err) } length := len(payload.Result) switch { case length == 0: logrus.Warningf("unable to find collection for env %s", envNumber) resp, err := UI.Ask("Enter collection name", UIOpts) if err != nil { log.Fatal(err) } resp2, err := UI.Ask("Enter move group name", UIOpts) if err != nil { log.Fatal(err) } return strings.ToLower(resp), strings.ToUpper(resp2) case length == 1: logrus.Debugf("using %s collection for env %s", payload.Result[0].Collection, envNumber) return payload.Result[0].Collection, payload.Result[0].ID default: logrus.Info("Environment in multiple active move-groups:") printOpts(payload.Result) var selected tools.Metadata for { resp, err := UI.Ask("Enter collection #", UIOpts) if err != nil { logrus.Error(err) continue } i, err := strconv.Atoi(resp) if err != nil { logrus.Error(err) continue } if len(payload.Result) < i || i < 1 { logrus.Error("select a valid number") continue } selected = payload.Result[i-1] break } return selected.Collection, selected.ID } } // GetAccountCollection is a simple helper function to parse and return a account collection func GetAccountCollection(accountID, token string) string { client := mongodbAPI.New(mongodbAPI.WithToken(token), "migration", false) acctInt, err := strconv.Atoi(accountID) if err != nil { logrus.Fatal(err) } byt, err := client.Find( &mongodbAPI.FindQuery{Query: map[string]interface{}{"accounts": acctInt, "active": true}}, tools.MetaCollection, ) if err != nil { logrus.Fatal(err) } var payload struct { Result []tools.Metadata `json:"result"` } // Use json.Decode for reading streams of JSON data if err := json.NewDecoder(bytes.NewReader(byt)).Decode(&payload); err != nil { logrus.Fatalf("gomigrate:GetAccountCollection:Decode:%s:%s", accountID, err) } length := len(payload.Result) switch { case length == 0: logrus.Warningf("unable to find collection for account %s", accountID) resp, err := UI.Ask("Enter collection name", UIOpts) if err != nil { log.Fatal(err) } return strings.ToLower(resp) case length == 1: logrus.Debugf("using %s collection for account %s", payload.Result[0].Collection, accountID) return payload.Result[0].Collection default: logrus.Infof("Account %s in multiple active move-groups:", accountID) printOpts(payload.Result) var selected tools.Metadata for { resp, err := UI.Ask("Enter collection #", UIOpts) if err != nil { logrus.Error(err) continue } i, err := strconv.Atoi(resp) if err != nil { logrus.Error(err) continue } if len(payload.Result) < i || i < 1 { logrus.Error("select a valid number") continue } selected = payload.Result[i-1] break } return selected.Collection } } // GetDCFromCollection is a simple helper function to parse and return // the DC name from a collection string // Typically in format: IAD2_01 func GetDCFromCollection(collection string) string { mgMatch := collectionRE.FindStringSubmatch(collection) // If we match example: IAD2_01 if len(mgMatch) == 2 { return strings.ToLower(mgMatch[1]) } return "" } // GetDCFromMoveGroup is a simple helper function to parse and return // the DC name from a move-group string // Typically in format: IAD2-MG-01 func GetDCFromMoveGroup(moveGroup string) string { mgMatch := moveGroupRE.FindStringSubmatch(moveGroup) // If we match example: IAD2-MG-01 if len(mgMatch) == 4 { return strings.ToLower(mgMatch[1]) } return "" } // EnvForDevice function will find the environment number for a given device // number. func EnvForDevice(device, token string) (dcxAutohost.DeviceEnvInfo, error) { i, err := strconv.Atoi(device) if err != nil { return dcxAutohost.DeviceEnvInfo{}, err } c, err := dcxAutohost.NewWithToken(token, false) if err != nil { return dcxAutohost.DeviceEnvInfo{}, err } info, err := c.DeviceEnvironment(i) if err != nil { return dcxAutohost.DeviceEnvInfo{}, err } return *info, nil } // PortMapsForDevice function will find the port-mappings for a given device // number. func PortMapsForDevice(device int, token string) (dcxAutohost.DevicePortMap, error) { mapping, err := migrationAPI.New(token, false).PortMaps([]int{device}) if err != nil || len(mapping) == 0 { return dcxAutohost.DevicePortMap{}, errors.Wrap(err, fmt.Sprintf("mapping result %d", len(mapping))) } return mapping[0], nil } // VlansForEnvironment function will find the environment vlan mapping for a given environment // number. func VlansForEnvironment(environment, token string) ([]dcxAutohost.EnvironmentMigrationVlan, error) { return migrationAPI.New(token, false).VlanTranslations(environment) } // MongoDevices will call out to Mongo to pull a list of devices within a move-group and can filter based on a custom filter. // if nil is passed as the filter, it will only return devices with current ports assigned func MongoDevices(moveGroups []string, accounts []int, devices []int, collection, token string, filter map[string]interface{}) ([]tools.Device, error) { var ( query = make(map[string]interface{}) // Make an empty response var deviceList = make([]tools.Device, 0) ) // Create a new Mongo Client c := mongodbAPI.New(mongodbAPI.WithToken(token), "migration", false) if len(moveGroups) != 0 { // Determine the mongo collection we should be querying based on the move-group name. if collection == "" { collection = GetCollection(moveGroups[0]) } if collection == "" { log.Fatalf("unable to parse collection name from %s", moveGroups[0]) } query = map[string]interface{}{ "move_group": map[string]interface{}{"$in": moveGroups}, "deleted": map[string]bool{"$ne": true}, } if len(accounts) != 0 { query["account.number"] = map[string]interface{}{"$in": accounts} } } else if len(devices) != 0 { if collection == "" { collection = GetDeviceCollection(devices[0], token) } query = map[string]interface{}{ "_id": map[string]interface{}{"$in": devices}, "deleted": map[string]bool{"$ne": true}, } } else if len(accounts) != 0 { if collection == "" { collection = GetAccountCollection(strconv.Itoa(accounts[0]), token) } query = map[string]interface{}{ "account.number": map[string]interface{}{"$in": accounts}, "deleted": map[string]bool{"$ne": true}, } } if filter == nil { query["environment.current.switch_ports_parsed"] = map[string]interface{}{"$nin": []interface{}{nil, []string{}}} } // Initiate the call out to the migration/Find resource byt, err := c.Find( &mongodbAPI.FindQuery{ Query: query, Projection: []string{ "move_group", "network", "device", "account.name", "account.number", "environment", "linked._id", "_table", }, }, collection) if err != nil { return deviceList, fmt.Errorf("gomigrate:getDeviceList:mongodbAPI.Find:%s", err) } // Decode the json response back var payload struct { Result []tools.Device `json:"result"` } jd := json.NewDecoder(strings.NewReader(string(byt))) if err = jd.Decode(&payload); err != nil { return deviceList, err } // Range through the response and only append network devices that // can be ran through shutdown onto the response values. return payload.Result, nil } // MongoFilters will call out to Mongo to pull a list of filters for a migration func MongoFilters(filterCollection string, token string) ([]MigrationFilter, error) { var ( query = make(map[string]interface{}) // Make an empty response var filterList = make([]MigrationFilter, 0) ) if filterCollection == "" { log.Fatal("MongoFilters(): filter collection name not defined") } // Create a new Mongo Client c := mongodbAPI.New(mongodbAPI.WithToken(token), "migration", false) // Initiate the call out to the migration/Find resource byt, err := c.Find( &mongodbAPI.FindQuery{ Query: query, }, filterCollection) if err != nil { return filterList, fmt.Errorf("gomigrate:getFilterList:mongodbAPI.Find:%s %s", filterCollection, err) } // Decode the json response back var payload struct { Result []MigrationFilter `json:"result"` } jd := json.NewDecoder(strings.NewReader(string(byt))) if err = jd.Decode(&payload); err != nil { return filterList, err } // Range through the response and only append network devices that // can be ran through shutdown onto the response values. return payload.Result, nil } // MongoNotes will call out to Mongo to pull a list of notes from the migration DBs customer notes collection. func MongoNotes(accounts []int, token string) ([]notes.Note, error) { var ( // Make an empty response var noteList = make([]notes.Note, 0) ) // Create a new Mongo Client c := mongodbAPI.New(mongodbAPI.WithToken(token), "migration", false) query := map[string]interface{}{ "account.number": map[string]interface{}{"$in": accounts}, } // Initiate the call out to the migration/Find resource byt, err := c.Find(&mongodbAPI.FindQuery{Query: query}, "customer") if err != nil { return noteList, fmt.Errorf("gomigrate:getDeviceList:mongodbAPI.Find:%s", err) } // Decode the json response back var payload struct { Result []notes.Note `json:"result"` } jd := json.NewDecoder(strings.NewReader(string(byt))) if err = jd.Decode(&payload); err != nil { return noteList, err } // Range through the response and only append network devices that // can be ran through shutdown onto the response values. return payload.Result, nil } // BridgeGroup type is the parsed data from the ParseBridgeGroup function type BridgeGroup map[string]struct { BridgeID string DestZone string } // ParseBridgeGroup function will take a bridge-group ID and parse it into a usable structure func ParseBridgeGroup(bridgeGroup string) BridgeGroup { var resp = make(BridgeGroup) match := bridgeGroupRE.FindStringSubmatch(bridgeGroup) // If we match example: BD-301A-1-20-501A-9-20 if len(match) == 9 { srcZone := fmt.Sprintf("aggr%s:Vlan%s", strings.Replace(match[1], "A", "", -1), match[4]) dstZone := fmt.Sprintf("aggr%s:Vlan%s", strings.Replace(match[5], "A", "", -1), match[8]) resp[srcZone] = struct { BridgeID string DestZone string }{BridgeID: match[0], DestZone: dstZone} return resp } return resp } // SortDevices will return a sorted []tools.Device list func SortDevices(d []tools.Device) []tools.Device { sort.Sort(tools.Devices(d)) return d } // SortPortMaps will return a sorted []*dcxAutohost.PortMapPair list func SortPortMaps(p []*dcxAutohost.PortMapPair) []*dcxAutohost.PortMapPair { sort.Sort(tools.PortMapPairs(p)) return p } // ConfirmPrompt is a help function to prompt the user for a confirmation. func ConfirmPrompt(s string) bool { reader := bufio.NewReader(os.Stdin) for { fmt.Printf("%s [y/n]: ", s) response, err := reader.ReadString('\n') if err != nil { log.Fatal(err) } response = strings.ToLower(strings.TrimSpace(response)) if response == "y" || response == "yes" { return true } else if response == "n" || response == "no" { return false } } } // VlanInSlice is a helper function to determine if a specific VLAN type already exist // within a slice of VLAN types. func VlanInSlice(vlan tools.VlanTranslation, slice []tools.VlanTranslation) bool { for _, s := range slice { if cmp.Equal(s, vlan) { return true } } return false } // ConcatVLANs function will take a list of int vlan numbers, sort the data, then // convert them to a list of summarized vlan strings for use in cli context // E.g. Input: []int{101, 102, 103, 105, 109, 110, 111} // Output: []string{"101-103", "105", "109-111"} func ConcatVLANs(vlans []int) (resp []string) { // Sort the integers so we can compare next/last // iterables sort.Ints(vlans) var start = 0 for i, current := range vlans { hasNext := false hasLast := false // If the next index is within range if i+1 != len(vlans) { hasNext = vlans[i+1] == current+1 } // If the last index is within range if i-1 != -1 { hasLast = vlans[i-1] == current-1 } // If the current iteration does not have a neighboring vlan in-front // or behind it, add this vlan id as an independent vlan. if !hasNext && !hasLast { resp = append(resp, strconv.Itoa(current)) continue } // If this iteration has a number in front, but not behind, // this can be considered the starting vlan. if hasLast && !hasNext { resp = append(resp, fmt.Sprintf("%d-%d", start, current)) start = 0 continue } // If this iteration has a number in front, but not behind, // this can be considered the starting vlan. if hasNext && !hasLast { start = current } } return resp } // printOpts func will take a structured type of response data and render a table func printOpts(data []tools.Metadata) { /* items := make(map[string]tools.Metadata) for i := range data { if _, ok := items[data[i].Collection]; !ok { items[data[i].Collection] = data[i] } }*/ t := table.NewWriter() t.SetOutputMirror(os.Stdout) t.AppendHeader(table.Row{"#", "Collection", "Description"}) for i, v := range data { t.AppendRow(table.Row{i + 1, v.Collection, v.Description}) } t.SetStyle(table.StyleColoredCyanWhiteOnBlack) t.SortBy([]table.SortBy{{Name: "#", Mode: table.Asc}}) t.Render() } // IContains is a case-insensitive contains search for a string func IContains(s, substr string) bool { return strings.Contains(strings.ToLower(s), strings.ToLower(substr)) } // ChunkDevices is a small internal function to break a list of Device types into // smaller chunks that we can send to go routines. func ChunkDevices(devList []tools.Device, size int) (chunk [][]tools.Device) { float := float64(len(devList)) / float64(size) chunkSize := int(math.Ceil(float)) for i := 0; i < len(devList); i += chunkSize { end := i + chunkSize if end > len(devList) { end = len(devList) } chunk = append(chunk, devList[i:end]) } return } // IntInSlice is a helper to dermine if a slice of int has a single integer in it. func IntInSlice(integer int, sl []int) bool { for i := range sl { if sl[i] == integer { return true } } return false } // MyDPClient will initialize the admin.mydatapipe.com client using the service account // we have stored in passwordsafe. func MyDPClient(ssoUser, ssoPasswd string) (*mydp.Client, error) { pwSafe, err := passwordsafe.NewClient(ssoUser, ssoPasswd) if err != nil { return nil, fmt.Errorf("passwordsafe.NewClient:%s", err) } pl, err := pwSafe.ProjectList() if err != nil { return nil, fmt.Errorf("pwSafe.ProjectList():%s", err) } for _, proj := range pl { if proj.Project.Id == 20834 { creds, err := pwSafe.ProjectCredentials(proj) if err != nil { return nil, fmt.Errorf("pwSafe.ProjectCredentials():%s", err) } for _, cr := range creds { if cr.Credential.Id == 181546 { return mydp.New(cr.Credential.Username, cr.Credential.Password, false) } } } } return nil, fmt.Errorf("[%s] unable to fetch datapipe service account", ssoUser) }