// 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 notes import ( "context" "errors" "fmt" "math" "mime/multipart" "net/http" "strconv" "strings" "sync" "time" "go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/bson/primitive" "github.com/sirupsen/logrus" "github.rackspace.com/DCM/migui/pkg/tools" "github.rackspace.com/gorax/clients/goctk" "go.mongodb.org/mongo-driver/bson" dcmTools "github.rackspace.com/DCM/tools" ) var ( errNoAccount = errors.New("no Account number specified in the Note payload") ) // CreateNote method will create a new Note record in the customerDB func (c *Client) CreateNote(note NotePayload, user string) error { var err error // The one requirement for a new note is the customer CORE Account number. // If the Account number is missing, return an error now. if note.Account.Number == 0 { return errNoAccount } account := note.Account if account.Name == "" { account, err = c.GetAccountInfo(note.Account.Number) if err != nil { return err } } timestamp := tools.Timestamp() newNote := bson.D{ {"account", account}, {"associated", bson.D{ {"move_group", strings.ToUpper(note.Assoc.MoveGroup)}, {"devices", note.Assoc.Devices}, {"ticket", note.Assoc.Ticket}, }}, {"data_center", strings.ToUpper(note.DataCenter)}, {"date_created", timestamp.Epoch()}, {"date_due", note.DateDue}, {"details", note.Details}, {"metadata", []string{}}, {"owner", note.Owner}, {"status", "NEW"}, {"summary", note.Summary}, {"team", strings.ToUpper(note.Team)}, {"type", strings.ToUpper(note.Type)}, {"updates", []update{ { Message: "Initial Creation", UpdatedBy: user, Timestamp: timestamp.Epoch(), }, }}, } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if _, err = c.mgoCol.InsertOne(ctx, newNote); err != nil { logrus.Errorf("migui.resource.CreateNote#InsertOne: %s", err) return err } return nil } // UpdateNote method will update and existing note record in the customerDB // // UpdateNote method will also return a non-cached response of the data. func (c *Client) UpdateNote(noteID, user string, note NotePayload) error { timestamp := tools.Timestamp() updated := bson.D{ {"$set", bson.D{ {"associated", bson.D{ {"move_group", note.Assoc.MoveGroup}, {"ticket", note.Assoc.Ticket}, }}, {"date_due", note.DateDue}, {"details", note.Details}, {"metadata", []string{}}, {"owner", note.Owner}, {"status", strings.ToUpper(note.Status)}, {"summary", note.Summary}, {"team", strings.ToUpper(note.Team)}, {"type", strings.ToUpper(note.Type)}, }}, {"$addToSet", bson.D{ {"updates", update{ Message: note.Update.Message, UpdatedBy: user, Timestamp: timestamp.Epoch(), }}, }}, } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() objID, err := primitive.ObjectIDFromHex(noteID) if err != nil { logrus.Errorf("migui.resource.UpdateNote#objID: %s", err) return err } if _, err := c.mgoCol.UpdateOne(ctx, bson.D{{"_id", objID}}, updated); err != nil { logrus.Errorf("migui.resource.UpdateNote#putUpdate: %s", err) return err } return nil } // UpdateStatus method will update status on an existing note in the customerDB func (c *Client) UpdateStatus(noteID, user, status string) error { timestamp := tools.Timestamp() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() objID, err := primitive.ObjectIDFromHex(noteID) if err != nil { logrus.Errorf("migui.resource.UpdateStatus#objID: %s", err) return err } if _, err := c.mgoCol.UpdateOne( ctx, bson.D{{"_id", objID}}, bson.D{ {"$set", bson.D{ {"status", strings.ToUpper(status)}, }}, {"$addToSet", bson.D{ {"updates", update{ Message: fmt.Sprintf("Status changed to %s", strings.ToUpper(status)), UpdatedBy: user, Timestamp: timestamp.Epoch(), }}, }}, }); err != nil { logrus.Errorf("migui.resource.UpdateStatus#putStatus: %s", err) return err } return nil } // ReadNotes method will gather the notes from the customerDB collection. // // ReadNotes method will also read/store note information in Redis if the cache-control values allow it. func (c *Client) ReadNotes(dataCenter, noteID string) (notes []Note, err error) { var payload = make([]Note, 0) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if dataCenter != "" { cursor, err := c.mgoCol.Find(ctx, bson.D{{"data_center", dataCenter}}) if err != nil { return []Note{}, err } if err = cursor.All(ctx, &payload); err != nil { return []Note{}, err } } else if noteID != "" { objID, err := primitive.ObjectIDFromHex(noteID) if err != nil { logrus.Errorf("migui.resource.ReadNotes#objID: %s", err) return []Note{}, err } cursor, err := c.mgoCol.Find(ctx, bson.D{{"_id", objID}}) if err != nil { return []Note{}, err } if err = cursor.All(ctx, &payload); err != nil { return []Note{}, err } } notes = payload return } // DeleteNote method will delete a single note in the customerDB func (c *Client) DeleteNote(noteID string) error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() objID, err := primitive.ObjectIDFromHex(noteID) if err != nil { logrus.Errorf("migui.resource.DeleteNote#objID: %s", err) return err } if _, err := c.mgoCol.DeleteOne(ctx, bson.D{{"_id", objID}}); err != nil { logrus.Errorf("migui.resource.DeleteNote: %s", err) return err } return nil } // GetAccountInfo method will pull information from CTK to populate the account-level items that // are tracked by this tool and populate the Account type. func (c *Client) GetAccountInfo(acctNum int) (Account, error) { res, err := c.ctk.AccountDetails(acctNum) if err != nil { logrus.Errorf("migui.resource.AccountDetails: %s", err) return Account{}, err } return Account{ LT: res.LeadTech, Manager: res.AccountManager, Name: res.Name, Number: func() int { x, err := strconv.Atoi(res.Number) if err != nil { return 0 } return x }(), PrimaryContact: res.PrimaryContact, TeamName: res.TeamName, NetSecTeam: res.NetSecTeam, MRR: strconv.FormatFloat(res.CMRR, 'f', 2, 64), }, nil } // CTKAccountSearch method will pull information from CTK and return a list of values that match the // partial account name search. func (c *Client) CTKAccountSearch(accountPart string) ([]goctk.AccountSFISearch, error) { data, err := c.ctk.AccountSearch(accountPart) if err != nil { logrus.Errorf("migui.resource.AccountSearch: %s", err) return []goctk.AccountSFISearch{}, err } return data, nil } // GetAccountMoveGroups method will pull information from the MigrationDB for a specific collection, to retrieve // the listing of move-groups for a migration. func (c *Client) GetAccountMoveGroups(collection string) (map[string][]string, error) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() cursor, err := c.db.Database(dbName).Collection(collection).Find( ctx, bson.D{ {"move_group", bson.D{{"$ne", nil}}}, {"deleted", bson.D{{"$ne", true}}}, }, options.Find().SetProjection(bson.D{{"move_group", 1}, {"account.number", 1}}), ) if err != nil { logrus.Errorf("migui.resource.GetMoveGroups: %s", err) return map[string][]string{}, err } payload := make([]dcmTools.Device, 0) if err = cursor.All(ctx, &payload); err != nil { logrus.Errorf("migui.resource.GetMoveGroups: %s", err) return map[string][]string{}, err } result := make(map[string][]string) DeviceLoop: for _, d := range payload { if strings.TrimSpace(d.MoveGroup) == "" { continue } acctNum := strconv.Itoa(d.Account.Number) if _, ok := result[acctNum]; !ok { result[acctNum] = make([]string, 0) } for _, mg := range result[acctNum] { if mg == d.MoveGroup { continue DeviceLoop } } result[acctNum] = append(result[acctNum], d.MoveGroup) } return result, nil } // SyncAccountData method will synchronize account information for the func (c *Client) SyncAccountData(accountID, user, dc string) (err error) { var ( // Make a default notes response to populate with mongo // data notes = make([]Note, 0) // Make a map of account -> note mappings so we know which accounts to // fetch data on accountSl = make([]int, 0) find = bson.D{} ) if dc != "" { find = bson.D{{"data_center", dc}} } if accountID != "" { find = bson.D{{"account.number", accountID}} } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() cursor, err := c.mgoCol.Find(ctx, find) if err != nil { return } if err = cursor.All(ctx, ¬es); err != nil { return } for _, note := range notes { inSlice := false for _, acct := range accountSl { if note.Account.Number == acct { inSlice = true break } } if !inSlice { accountSl = append(accountSl, note.Account.Number) } } var wg sync.WaitGroup for _, acctChunk := range chunk(accountSl, 3) { for _, acct := range acctChunk { wg.Add(1) go func(ac int) { var a Account if err = goctk.Retry(5, 2*time.Second, func() error { res, err := c.ctk.AccountDetails(ac) if err != nil { return err } a = Account{ LT: res.LeadTech, Manager: res.AccountManager, Name: res.Name, Number: func() int { x, err := strconv.Atoi(res.Number) if err != nil { return 0 } return x }(), PrimaryContact: res.PrimaryContact, TeamName: res.TeamName, NetSecTeam: res.NetSecTeam, MRR: strconv.FormatFloat(res.CMRR, 'f', 2, 64), } return nil }); err != nil { logrus.Errorf("migui.resource.SyncAccountData#GetAccountInfo: %s", err) wg.Done() return } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if _, err = c.mgoCol.UpdateMany( ctx, bson.D{{"account.number", ac}}, bson.D{{"$set", bson.D{{"account", a}}}}); err != nil { logrus.Errorf("migui.resource.SyncAccountData#UpdateAll: %s", err) } wg.Done() }(acct) } wg.Wait() } logrus.Infof("migui.resource.SyncAccountData#Refresh of %d accounts completed", len(accountSl)) return nil } // CreateAttachment method will post a new attachment to the back-end storage resource func (c *Client) CreateAttachment(o tools.ObjectStore, noteID, name, user string, file multipart.File) (err error) { if err = o.Create(name, noteID, file); err != nil { logrus.Errorf("migui.resource.CreateAttachment#Create: %s", err) return err } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() objID, err := primitive.ObjectIDFromHex(noteID) if err != nil { logrus.Errorf("migui.resource.CreateAttachment#objID: %s", err) return err } if _, err = c.mgoCol.UpdateOne( ctx, bson.D{{"_id", objID}}, bson.D{{"$addToSet", bson.D{ {"attachments", name}, {"updates", update{ Message: fmt.Sprintf("File attached: %s", name), UpdatedBy: user, Timestamp: tools.Timestamp().Epoch(), }}, }}}); err != nil { logrus.Errorf("migui.resource.CreateAttachment#UpdateId: %s", err) } return err } // GetAttachment method will download the selected attachment func (c *Client) GetAttachment(o tools.ObjectStore, noteID, name, user string) ([]byte, http.Header, error) { byt, headers, err := o.Get(name, noteID) if err != nil { logrus.Errorf("migui.resource.GetAttachment#get: %s", err) return nil, nil, err } return byt, headers, err } // DeleteAttachment method will remove and unassociate the attachment from the note_id func (c *Client) DeleteAttachment(o tools.ObjectStore, noteID, name, user string) (err error) { if err = o.Delete(name, noteID); err != nil { logrus.Errorf("migui.resource.DeleteAttachment#Delete: %s", err) return err } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() objID, err := primitive.ObjectIDFromHex(noteID) if err != nil { logrus.Errorf("migui.resource.DeleteAttachment#objID: %s", err) return err } if _, err = c.mgoCol.UpdateOne( ctx, bson.D{{"_id", objID}}, bson.D{ {"$pull", bson.D{{"attachments", name}}}, {"$addToSet", bson.D{{"updates", update{ Message: fmt.Sprintf("File removed: %s", name), UpdatedBy: user, Timestamp: tools.Timestamp().Epoch(), }}}}, }); err != nil { logrus.Errorf("migui.resource.DeleteAttachment#UpdateId: %s", err) } return err } // chunk is a small internal function to break a list of string types into // smaller chunks that we can send to go routines. func chunk(strs []int, size int) (chunk [][]int) { float := float64(len(strs)) / float64(size) chunkSize := int(math.Ceil(float)) for i := 0; i < len(strs); i += chunkSize { end := i + chunkSize if end > len(strs) { end = len(strs) } chunk = append(chunk, strs[i:end]) } return }