// 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 processmaps is a helper function to manage the processMaps migration collection. package processmaps import ( "bytes" "context" "encoding/json" "fmt" "log" "net/http" "os" "strconv" "strings" "time" "github.com/jedib0t/go-pretty/v6/table" "github.com/jokelyo/argparse" "github.com/sirupsen/logrus" input "github.com/tcnksm/go-input" "github.rackspace.com/DCM/gomigrate/core" "github.rackspace.com/DCM/gomigrate/shared" "github.rackspace.com/gorax/gokeys" ) var ( ui = &input.UI{Writer: os.Stdout, Reader: os.Stdin} uiOpts = &input.Options{Required: true, HideOrder: true} uiOptional = &input.Options{Required: false, HideOrder: true} addFlag *bool ) type ( mapping struct { ID string `json:"_id"` Order int `json:"order"` ProcessType string `json:"process_os_type"` Where []where `json:"Where"` } where struct { Name string `json:"name"` Value string `json:"value"` } ) // 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 DCX Translations logic cmd := p.NewCommand("processmap", "Find or modify process mappings.") addFlag = cmd.Flag("", "add", &argparse.Options{Help: "add a process-map"}) return core.Plugin{CMD: cmd, Func: pluginFunc} } // pluginFunc function is executed from the gomigrate caller func pluginFunc(cfg gokeys.Settings) { // Pull an identity token from the users gokeys keychain key, err := cfg.Identity(false) if err != nil { log.Fatal(err) } if *addFlag { addProcessMap(key.Password) return } search(key.Password) } // pPrint func will take a structured type of response data and render a table // output to stdout. func pPrint(items []mapping) { t := table.NewWriter() t.SetOutputMirror(os.Stdout) for _, i := range items { name := "ID\nOrder\nprocess.os_type" value := i.ID + "\n" + strconv.Itoa(i.Order) + "\n" + i.ProcessType for _, w := range i.Where { name += fmt.Sprintf("\n%s", w.Name) value += fmt.Sprintf("\n%s", w.Value) } t.AppendRow(table.Row{strings.ToUpper(name), value}) } t.SortBy([]table.SortBy{{Name: "Order", Mode: table.Dsc}}) t.SetStyle(table.StyleDefault) t.Style().Options.SeparateRows = true t.Render() } // processMapAPI function will interface with the process-maps API to get/put data. func get(token string) ([]mapping, error) { var ( headers = map[string]string{ "Accept": "application/json", "X-Auth-Token": token, } result struct { Data []mapping `json:"data"` Error string `json:"error"` } ) ctx, cancel := context.WithTimeout(context.Background(), time.Second*60) defer cancel() // Build the request req, err := http.NewRequestWithContext(ctx, http.MethodGet, shared.MigAPI+"/process/maps", nil) if err != nil { logrus.Errorf("get:NewRequest:%s", err) return result.Data, err } for key, value := range headers { req.Header.Set(key, value) } // Send the request via a client // Do sends an HTTP request and // returns an HTTP response r, err := http.DefaultClient.Do(req) if err != nil { logrus.Errorf("get:Do:%s", err) return result.Data, err } if r != nil { defer func() { if defErr := r.Body.Close(); defErr != nil { err = fmt.Errorf("%s/%s", err, defErr) } }() } if r.StatusCode > http.StatusNoContent { logrus.Errorf("resp_err:%s", r.Status) return result.Data, err } // Use json.Decode for reading streams of JSON data if err := json.NewDecoder(r.Body).Decode(&result); err != nil { logrus.Errorf("get:Decode:%s", err) return result.Data, err } return result.Data, nil } // processMapAPI function will interface with the process-maps API to get/put data. func post(token string, pmap mapping) error { var headers = map[string]string{ "Accept": "application/json", "X-Auth-Token": token, } btyPayload, err := json.Marshal(pmap) if err != nil { logrus.Errorf("post:json.Marshal:%s", err) return err } ctx, cancel := context.WithTimeout(context.Background(), time.Second*60) defer cancel() // Build the request req, err := http.NewRequestWithContext(ctx, http.MethodPost, shared.MigAPI+"/process/maps", bytes.NewReader(btyPayload)) if err != nil { logrus.Errorf("post:NewRequest:%s", err) return err } for key, value := range headers { req.Header.Set(key, value) } // Send the request via a client // Do sends an HTTP request and // returns an HTTP response r, err := http.DefaultClient.Do(req) if err != nil { logrus.Errorf("post:Do:%s", err) return err } if r != nil { defer func() { if defErr := r.Body.Close(); defErr != nil { err = fmt.Errorf("%s/%s", err, defErr) } }() } if r.StatusCode > http.StatusNoContent { logrus.Errorf("resp_err:%s", r.Status) return err } return nil } func search(key string) { var search string for { searchResp, err := ui.Ask("Search by process.os_type or device.make_model", uiOpts) if err != nil { log.Fatal(err) } search = strings.Trim(searchResp, " ") if search != "" { break } } results, err := get(key) if err != nil { log.Fatal(err) } resp := make([]mapping, 0) for _, result := range results { if !strings.Contains(strings.ToLower(result.ProcessType), strings.ToLower(search)) { continue } for _, w := range result.Where { if !strings.Contains(strings.ToLower(w.Value), strings.ToLower(search)) { continue } } resp = append(resp, result) } pPrint(resp) } func addProcessMap(key string) { var ( update mapping wheres = make(map[string]string) ) logrus.Info("Retrieving existing process maps ...") results, err := get(key) if err != nil { log.Fatal(err) } for { fmt.Println("Enter db keys and values to match process type. [Enter to exit]") dbKey, err := ui.Ask("Enter DB key name", uiOptional) if err != nil { logrus.Fatal(err) } dbKey = strings.Trim(dbKey, " ") if dbKey == "" { break } dbValue, err := ui.Ask("Enter value", uiOpts) if err != nil { logrus.Fatal(err) } dbValue = strings.Trim(dbValue, " ") if dbValue == "" { logrus.Errorf("you must enter a value!") continue } wheres[dbKey] = dbValue } for { typeResp, err := ui.Ask("Process type name", uiOpts) if err != nil { logrus.Fatal(err) } update.ProcessType = strings.Trim(typeResp, " ") if update.ProcessType != "" { break } } for { orderResp, err := ui.Ask("Enter order [Blank to use last]", uiOptional) if err != nil { log.Fatal(err) } i, err := strconv.Atoi(orderResp) if err != nil { update.Order = 0 break } match := false for _, r := range results { if r.Order == i { match = true break } } if !match { update.Order = i break } logrus.Warn("Order number already in use.") } if update.Order == 0 { results, err := get(key) if err != nil { log.Fatal(err) } iOrder := results[len(results)-1].Order update.Order = iOrder + 10 } for k, v := range wheres { update.Where = append(update.Where, where{Name: strings.ToLower(k), Value: v}) } if err := post(key, update); err != nil { logrus.Fatal(err) } log.Printf("Successfully added %s to process-map DB at position %d\n", update.ProcessType, update.Order) }