// 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 snapshot import ( "context" "crypto/tls" "encoding/json" "fmt" "log" "net/http" "net/url" "strconv" "sync" "time" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.rackspace.com/DCM/gomigrate/core" "github.rackspace.com/DCM/gomigrate/shared" "github.rackspace.com/gorax/gokeys" ) const ( timeout = 1800 ) type ( // macSnapshotResp type is a representation of // a response from the putMacAddresses call macSnapshotResp struct { Data struct { DeviceID string `json:"device_number"` AccountNumber string `json:"account_number"` AccountName string `json:"account_name"` MoveGroup string `json:"move_group"` Deleted string `json:"device_status"` DeviceModel string `json:"device_model"` SummaryPorts []string `json:"summary_ports"` SwitchPorts []switchPorts `json:"switch_ports"` } `json:"data"` Error string `json:"error"` } switchPorts struct { SwitchPorts string `json:"switch_name"` Service string `json:"switch_service"` Interface string `json:"interface"` Duplex string `json:"duplex"` FePort int `json:"fe_port"` Mode string `json:"mode"` Admin string `json:"admin"` Link string `json:"link"` Speed string `json:"speed"` Macs []string `json:"macs"` } routine struct { *sync.WaitGroup col string } ) var ( argAccounts *[]string argDevices *[]int argMoveGroups *[]string flagClean *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("snapshot", "Runs a MAC Address snapshot") argAccounts = shared.ArgAccounts(cmd) argDevices = shared.ArgDevices(cmd) argMoveGroups = shared.ArgMoveGroups(cmd) flagClean = shared.ArgFlag(cmd, "clean", "Remove the snapshot for a list of move-groups") 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) { coreKey, err := cfg.Core(false) if err != nil { log.Fatal(err) } // Pull an identity token from the users gokeys keychain key, err := cfg.Identity(false) if err != nil { log.Fatal(err) } moveGroupList := *argMoveGroups if len(moveGroupList) == 0 && len(*argAccounts) == 0 && len(*argDevices) == 0 { log.Fatal("must specify at least one device, account, or move-group") } if *flagClean { for _, moveGroup := range moveGroupList { if err := deleteMacAddresses(moveGroup, key.Password); err != nil { logrus.Error(err) } } logrus.Infof("mac-address-snapshot cleanup for %s complete", moveGroupList) return } var collection string if len(*argDevices) != 0 { if len(*argAccounts) != 0 || len(moveGroupList) != 0 { logrus.Warn("preferring device list over account or mg list") } collection = shared.GetDeviceCollection((*argDevices)[0], coreKey.Password) var r = routine{ WaitGroup: new(sync.WaitGroup), col: collection, } for _, d := range *argDevices { r.Add(1) go r.putMacAddresses("", key.Password, d, 0) } r.Wait() } else if len(*argAccounts) != 0 { if len(moveGroupList) != 0 { logrus.Warn("preferring account over mg list") } collection = shared.GetAccountCollection((*argAccounts)[0], coreKey.Password) var r = routine{ WaitGroup: new(sync.WaitGroup), col: collection, } for _, a := range *argAccounts { acct, err := strconv.Atoi(a) if err != nil { continue } r.Add(1) go r.putMacAddresses("", key.Password, 0, acct) } r.Wait() } else { // Determine the mongo collection we should be querying based on the move-group // name. collection = shared.GetCollection(moveGroupList[0]) if collection == "" { log.Fatalf("unable to parse collection name from %s", moveGroupList[0]) } if len(moveGroupList) > 3 { logrus.Fatal("you can only run a maximum of 3 move-groups at a time") } var r = routine{ WaitGroup: new(sync.WaitGroup), col: collection, } for _, moveGroup := range moveGroupList { r.Add(1) go r.putMacAddresses(moveGroup, key.Password, 0, 0) } r.Wait() } } // putMacAddresses function will take a device and reach out to the migration service to capture // switch port mac-addresses and put them into a table for troubleshooting purposes. func (r *routine) putMacAddresses(moveGroup, token string, device, account int) { defer r.Done() out := moveGroup if device != 0 { out = strconv.Itoa(device) } else if account != 0 { out = strconv.Itoa(account) } logrus.Infof("starting mac capture for %s. this might take some time.", out) var headers = map[string]string{ "Accept": "application/json", "X-Auth-Token": token, } // QueryEscape escapes the device ID string so // it can be safely placed inside a URL query escMG := url.QueryEscape(moveGroup) fullURL := fmt.Sprintf("https://migration.nsi.rackspace.com/collect/mac_snapshot/%s/current", r.col) if device != 0 { fullURL += fmt.Sprintf("?device=%d", device) } else if account != 0 { fullURL += fmt.Sprintf("?account=%d", account) } else { fullURL += fmt.Sprintf("?move_group=%s", escMG) } ctx, cancel := context.WithTimeout(context.Background(), time.Second*timeout) defer cancel() // Build the request req, err := http.NewRequestWithContext(ctx, http.MethodPut, fullURL, nil) if err != nil { logrus.Errorf("putMacAddresses:NewRequest:%s:%s", out, err) return } for key, value := range headers { req.Header.Set(key, value) } // For control over HTTP client headers, // redirect policy, and other settings, // create a Client // A Client is an HTTP client client := &http.Client{ Transport: &http.Transport{ // nolint:gosec TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, } // Send the request via a client // Do sends an HTTP request and // returns an HTTP response resp, err := client.Do(req) if err != nil { logrus.Errorf("putMacAddresses:Do:%s:%s", out, err) return } // Callers should close resp.Body // when done reading from it // Defer the closing of the body defer func() { if defErr := resp.Body.Close(); defErr != nil { err = errors.Wrap(defErr, "defer error") } }() // Fill the record with the data from the JSON var data macSnapshotResp // Use json.Decode for reading streams of JSON data if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { if err.Error() != "EOF" { logrus.Errorf("putMacAddresses:Decode:%s:%s", out, err) logrus.Infof("mac capture complete with errors for %s", out) } //return } if resp.StatusCode > 204 { logrus.Errorf("%s %s", resp.Status, data.Error) logrus.Infof("mac capture complete with errors for %s", out) return } logrus.Infof("mac capture complete for %s", out) } // deleteMacAddresses function will take a move-group and remove all matches from the snapshot database collection. func deleteMacAddresses(moveGroup, token string) error { logrus.Infof("removing mac-address snapshot for %s", moveGroup) var headers = map[string]string{ "Accept": "application/json", "Connection": "close", "X-Auth-Token": token, } // QueryEscape escapes the device ID string so // it can be safely placed inside a URL query escMG := url.QueryEscape(moveGroup) fullURL := fmt.Sprintf("https://migration.nsi.rackspace.com/clean/mac_snapshot?move_group=%s", escMG) ctx, cancel := context.WithTimeout(context.Background(), time.Second*60) defer cancel() // Build the request req, err := http.NewRequestWithContext(ctx, http.MethodDelete, fullURL, nil) if err != nil { return fmt.Errorf("deleteMacAddresses:NewRequest:%s:%s", escMG, err) } for key, value := range headers { req.Header.Set(key, value) } // For control over HTTP client headers, // redirect policy, and other settings, // create a Client client := &http.Client{ Transport: &http.Transport{ // nolint:gosec TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, } // Send the request via a client // Do sends an HTTP request and // returns an HTTP response resp, err := client.Do(req) if err != nil { return fmt.Errorf("deleteMacAddresses:Do:%s:%s", escMG, err) } // Callers should close resp.Body // when done reading from it // Defer the closing of the body defer func() { if defErr := resp.Body.Close(); defErr != nil { err = errors.Wrap(defErr, "defer error") } }() if resp.StatusCode != http.StatusNoContent { return fmt.Errorf("recieved unsuccessful response code back for %s:%d", escMG, resp.StatusCode) } return nil }