diff --git a/cmd/climc/shell/contacts.go b/cmd/climc/shell/contacts.go index 76d4e75e46..79a2a79e44 100644 --- a/cmd/climc/shell/contacts.go +++ b/cmd/climc/shell/contacts.go @@ -29,8 +29,8 @@ func init() { */ type ContactsUpdateOptions struct { UID string `help:"The user you wanna add contact to (Keystone User ID)"` - CONTACTTYPE string `help:"The contact type" choices:"email|mobile"` - CONTACT string `help:"The contacts details mobile number or email address, if set it the empty str means delete"` + CONTACTTYPE string `help:"The contact type email|mobile" choices:"email|mobile|dingtalk"` + CONTACT string `help:"The contacts details mobile number or email address or dingtalk's userid, if set it the empty str means delete"` Status string `help:"Enabled or disabled contact status" choices:"enable|disable"` } R(&ContactsUpdateOptions{}, "contact-update", "Create, delete or update contact for user", func(s *mcclient.ClientSession, args *ContactsUpdateOptions) error { @@ -40,9 +40,9 @@ func init() { tmpObj.Add(jsonutils.NewString(args.CONTACT), "contact") if len(args.Status) > 0 { if args.Status == "disable" { - tmpObj.Add(jsonutils.JSONFalse, "enabled") + tmpObj.Add(jsonutils.NewInt(0), "enabled") } else { - tmpObj.Add(jsonutils.JSONTrue, "enabled") + tmpObj.Add(jsonutils.NewInt(1), "enabled") } } @@ -63,7 +63,7 @@ func init() { type ContactsDeleteOptions struct { UID string `help:"The user you wanna add contact to (Keystone User ID)"` - CONTACTTYPE string `help:"The contact type" choices:"email|mobile"` + CONTACTTYPE string `help:"The contact type email|mobile|dingtalk" choices:"email|mobile|dingtalk"` } R(&ContactsDeleteOptions{}, "contact-delete", "Delete contact for user", func(s *mcclient.ClientSession, args *ContactsDeleteOptions) error { arr := jsonutils.NewArray() diff --git a/cmd/climc/shell/notification.go b/cmd/climc/shell/notification.go index eab6f54874..3974c8649c 100644 --- a/cmd/climc/shell/notification.go +++ b/cmd/climc/shell/notification.go @@ -93,7 +93,7 @@ func init() { STATUS string `help:"Notification send status" choices:"sent_ok|send_fail"` Remark string `help:"Remark or description of the operation or fail reason"` } - R(&NotificationUpdateCallbackOptions{}, "notification-update-callback", "Update send status of the notification task", func(s *mcclient.ClientSession, args *NotificationUpdateCallbackOptions) error { + R(&NotificationUpdateCallbackOptions{}, "notification-update-callback", "UpdateItem send status of the notification task", func(s *mcclient.ClientSession, args *NotificationUpdateCallbackOptions) error { params := jsonutils.NewDict() params.Add(jsonutils.NewString(args.STATUS), "status") if len(args.Remark) > 0 { diff --git a/cmd/climc/shell/notify.go b/cmd/climc/shell/notify.go new file mode 100644 index 0000000000..66ac849c2d --- /dev/null +++ b/cmd/climc/shell/notify.go @@ -0,0 +1,59 @@ +// Copyright 2019 Yunion +// +// 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 shell + +import ( + "yunion.io/x/jsonutils" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/mcclient/modules" +) + +func init() { + + type ConfigCreate2Options struct { + CONTACTTYPE string `help:"contact type (email|sms_aliyun or others)"` + CONFIGS []string `help:"config (k, v)"` + } + R(&ConfigCreate2Options{}, "notify-config-update", "config update, example: notify_config-update email mail.smtp.hostname hostname mail.smtp.hostport 123.", func(s *mcclient.ClientSession, args *ConfigCreate2Options) error { + tmp := jsonutils.NewDict() + for i := 0; i+1 < len(args.CONFIGS); i += 2 { + tmp.Add(jsonutils.NewString(args.CONFIGS[i+1]), args.CONFIGS[i]) + } + body := jsonutils.NewDict() + body.Add(tmp, args.CONTACTTYPE) + modules.Configs.Create(s, body) + return nil + }) + + type ConfigGet2Options struct { + TYPE string `help:"contact type (email|sms_aliyun or others)"` + } + R(&ConfigGet2Options{}, "notify-config-show", "config show", func(s *mcclient.ClientSession, args *ConfigGet2Options) error { + result, err := modules.Configs.Get(s, args.TYPE, jsonutils.JSONNull) + if err != nil { + return err + } + printObject(result) + return nil + }) + R(&ConfigGet2Options{}, "notify-config-delete", "config delete", func(s *mcclient.ClientSession, args *ConfigGet2Options) error { + result, err := modules.Configs.Delete(s, args.TYPE, jsonutils.JSONNull) + if err != nil { + return err + } + printObject(result) + return nil + }) +} diff --git a/cmd/notify/main.go b/cmd/notify/main.go new file mode 100644 index 0000000000..8b5a58fd14 --- /dev/null +++ b/cmd/notify/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "yunion.io/x/onecloud/pkg/notify" +) + +func main() { + notify.StartService() +} diff --git a/pkg/compute/models/guest_actions.go b/pkg/compute/models/guest_actions.go index 995214cd2f..be1d9ac012 100644 --- a/pkg/compute/models/guest_actions.go +++ b/pkg/compute/models/guest_actions.go @@ -3047,7 +3047,7 @@ func (self *SGuest) doSaveRenewInfo(ctx context.Context, userCred mcclient.Token return nil }) if err != nil { - log.Errorf("Update error %s", err) + log.Errorf("UpdateItem error %s", err) return err } db.OpsLog.LogEvent(self, db.ACT_RENEW, self.GetShortDesc(ctx), userCred) diff --git a/pkg/compute/models/snapshotpolicy.go b/pkg/compute/models/snapshotpolicy.go index 786c4a9116..65650d5822 100644 --- a/pkg/compute/models/snapshotpolicy.go +++ b/pkg/compute/models/snapshotpolicy.go @@ -404,4 +404,4 @@ func (self *SSnapshotPolicy) preCheck( return nil, httperrors.NewNotFoundError("Disks %v not found", notFoundDisks) } return diskIds, nil -} +} \ No newline at end of file diff --git a/pkg/lbagent/gobetween/config.go b/pkg/lbagent/gobetween/config.go index 49f1d45f31..4352d61afe 100644 --- a/pkg/lbagent/gobetween/config.go +++ b/pkg/lbagent/gobetween/config.go @@ -13,7 +13,7 @@ // limitations under the License. /** - * config.go - config file definitions + * mod_config.go - config file definitions * * @author Yaroslav Pogrebnyak * @author Gene Ponomarenko diff --git a/pkg/mcclient/modules/mod_notify.go b/pkg/mcclient/modules/mod_notify.go new file mode 100644 index 0000000000..1827b05200 --- /dev/null +++ b/pkg/mcclient/modules/mod_notify.go @@ -0,0 +1,17 @@ +package modules + +type ConfigsManager struct { + ResourceManager +} + +var ( + Configs ConfigsManager +) + +func init() { + Configs = ConfigsManager{NewNotifyManager("config", "configs", + []string{}, + []string{})} + + register(&Configs) +} diff --git a/pkg/notify/compatible_config.go b/pkg/notify/compatible_config.go new file mode 100644 index 0000000000..074ba5b209 --- /dev/null +++ b/pkg/notify/compatible_config.go @@ -0,0 +1,184 @@ +// Copyright 2019 Yunion +// +// 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 notify + +import ( + "context" + "net/http" + "strconv" + + "yunion.io/x/jsonutils" + + "yunion.io/x/onecloud/pkg/appsrv" + "yunion.io/x/onecloud/pkg/httperrors" +) + +const ( + EMAIL_KEYWORDPLURAL = "email_configs" + EMAIL_KEYWORD = "email_config" + EMAIL = "email" + + SMS_KEYWORDPLURAL = "sms_configs" + SMS_KEYWORD = "sms_config" + SMS = "mobile" +) + +func emailConfigDeleteHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + // do not need modify + manager, _, _, _ := fetchEnv(ctx, w, r) + params := map[string]string{ + "": EMAIL, + } + err := manager.DeleteConfig(ctx, params) + if err != nil { + httperrors.GeneralServerError(w, err) + } +} + +func emailConfigGetHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + manager, _, query, _ := fetchEnv(ctx, w, r) + params := map[string]string{ + "": EMAIL, + } + ret, err := manager.GetConfig(ctx, params, query) + if err != nil { + httperrors.GeneralServerError(w, err) + } + // hostport should be int and ssl_global should be bool + newDataDict := make(map[string]interface{}) + data, _ := ret.Get("config") + dataDict := data.(*jsonutils.JSONDict) + dataDict = database2Display(dataDict) + for _, k := range dataDict.SortedKeys() { + tmp, _ := dataDict.GetString(k) + switch k { + case "hostport": + port, _ := strconv.Atoi(tmp) + newDataDict[k] = port + case "ssl_global": + ssl, _ := strconv.ParseBool(tmp) + newDataDict[k] = ssl + default: + newDataDict[k] = tmp + } + } + appsrv.SendJSON(w, jsonutils.Marshal(map[string]map[string]interface{}{ + EMAIL_KEYWORD: newDataDict, + })) +} + +func dispaly2Database(dict *jsonutils.JSONDict) *jsonutils.JSONDict { + keys := dict.SortedKeys() + newKey := "" + for _, key := range keys { + switch key { + case "username", "password": + newKey = "mail." + key + case "hostname", "hostport": + newKey = "mail.smtp." + key + case "ssl_global": + newKey = "mail.global.ssl" + } + v, _ := dict.Get(key) + dict.Add(v, newKey) + dict.Remove(key) + } + return dict +} + +func database2Display(dict *jsonutils.JSONDict) *jsonutils.JSONDict { + keys := dict.SortedKeys() + newKey := "" + for _, key := range keys { + switch key { + case "mail.username", "mail.password": + newKey = key[5:] + case "mail.smtp.hostname", "mail.smtp.hostport": + newKey = key[10:] + case "mail.global.ssl": + newKey = "ssl_global" + } + v, _ := dict.Get(key) + dict.Add(v, newKey) + dict.Remove(key) + } + return dict +} + +func emailConfigUpdateHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + manager, _, _, body := fetchEnv(ctx, w, r) + body, _ = body.Get(EMAIL_KEYWORD) + bodyRet := jsonutils.DeepCopy(body) + bodyDict := body.(*jsonutils.JSONDict) + newBody := jsonutils.NewDict() + newBody.Add(dispaly2Database(bodyDict), EMAIL) + err := manager.UpdateConfig(ctx, newBody) + if err != nil { + httperrors.GeneralServerError(w, err) + } + data := jsonutils.NewDict() + data.Add(jsonutils.NewInt(200), "code") + data.Add(jsonutils.NewString("OK"), "title") + data.Add(bodyRet, "message") + ret := jsonutils.NewDict() + ret.Add(data, EMAIL_KEYWORD) + appsrv.SendJSON(w, ret) +} + +func smsConfigDeleteHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + // do not need modify + manager, _, _, _ := fetchEnv(ctx, w, r) + params := map[string]string{ + "": SMS, + } + err := manager.DeleteConfig(ctx, params) + if err != nil { + httperrors.GeneralServerError(w, err) + } +} + +func smsConfigGetHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + manager, _, query, _ := fetchEnv(ctx, w, r) + params := map[string]string{ + "": SMS, + } + ret, err := manager.GetConfig(ctx, params, query) + if err != nil { + httperrors.GeneralServerError(w, err) + } + // modify + data, _ := ret.Get("config") + appsrv.SendJSON(w, jsonutils.Marshal(map[string]jsonutils.JSONObject{ + SMS_KEYWORD: data, + })) +} + +func smsConfigUpdateHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + manager, _, _, body := fetchEnv(ctx, w, r) + body, _ = body.Get(SMS_KEYWORD) + newBody := jsonutils.NewDict() + newBody.Add(body, SMS) + err := manager.UpdateConfig(ctx, newBody) + if err != nil { + httperrors.GeneralServerError(w, err) + } + data := jsonutils.NewDict() + data.Add(jsonutils.NewInt(200), "code") + data.Add(jsonutils.NewString("OK"), "title") + data.Add(body, "message") + ret := jsonutils.NewDict() + ret.Add(data, SMS_KEYWORD) + appsrv.SendJSON(w, ret) +} diff --git a/pkg/notify/dispatcher.go b/pkg/notify/dispatcher.go new file mode 100644 index 0000000000..de92af0e1e --- /dev/null +++ b/pkg/notify/dispatcher.go @@ -0,0 +1,513 @@ +// Copyright 2019 Yunion +// +// 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 notify + +import ( + "bytes" + "context" + "fmt" + "net/http" + "strings" + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/utils" + + "yunion.io/x/onecloud/pkg/appctx" + "yunion.io/x/onecloud/pkg/appsrv" + "yunion.io/x/onecloud/pkg/appsrv/dispatcher" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/policy" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/notify/models" + "yunion.io/x/onecloud/pkg/util/rbacutils" +) + +// NotifyModelDispatcher is designed to complete some function that db.DBModelDispatcher can't. +// The apis of notify module has a certain degree of particularity so that we can't use common function. +type NotifyModelDispatcher struct { + db.DBModelDispatcher +} + +func NewNotifyModelDispatcher(manager db.IModelManager) *NotifyModelDispatcher { + return &NotifyModelDispatcher{*db.NewModelHandler(manager)} +} + +func (self *NotifyModelDispatcher) GetConfig(ctx context.Context, params map[string]string, query jsonutils.JSONObject) (jsonutils.JSONObject, error) { + listResult, err := self.List(ctx, mergeQueryParams(params, query), nil) + if err != nil { + return nil, err + } + keyVs := make(map[string]string) + for _, ret := range listResult.Data { + key, _ := ret.GetString("key_text") + value, _ := ret.GetString("value_text") + keyVs[key] = value + } + return jsonutils.Marshal(map[string]map[string]string{ + models.ConfigManager.Keyword(): keyVs, + }), nil +} + +func (self *NotifyModelDispatcher) DeleteConfig(ctx context.Context, params map[string]string) error { + contactType := params[""] + configs, err := models.ConfigManager.GetConfigByType(contactType) + if err != nil { + return errors.Wrap(err, "Get Config by contactType failed") + } + userCred := policy.FetchUserCredential(ctx) + for i := range configs { + err = DeleteItem(models.ConfigManager, &configs[i], ctx, userCred, jsonutils.JSONNull, jsonutils.JSONNull) + if err != nil { + return errors.Wrapf(err, "Delete part of old one, so please input new data again.") + } + } + return nil +} + +// UpdateConfig update config and restart corresponding send service. +func (self *NotifyModelDispatcher) UpdateConfig(ctx context.Context, body jsonutils.JSONObject) error { + data := body.(*jsonutils.JSONDict) + contactType := data.SortedKeys()[0] + originData, err := models.ConfigManager.GetVauleByType(contactType) + if err != nil { + return err + } + tmp, _ := data.Get(contactType) + data = tmp.(*jsonutils.JSONDict) + userCred := policy.FetchUserCredential(ctx) + // If no config of type 'contactType' in database, create news. + // Else delete original ones and create news. + if len(originData) != 0 { + // delete original + configs, err := models.ConfigManager.GetConfigByType(contactType) + if err != nil { + return errors.Wrap(err, "Get Config by contactType failed") + } + for i := range configs { + err = DeleteItem(models.ConfigManager, &configs[i], ctx, userCred, jsonutils.JSONNull, jsonutils.JSONNull) + if err != nil { + return errors.Wrapf(err, "Delete part of old one, so please input new data again.") + } + } + } + // create + for _, key := range data.SortedKeys() { + createData := jsonutils.NewDict() + tmp, _ = data.Get(key) + createData.Add(tmp, "value_text") + createData.Add(jsonutils.NewString(key), "key_text") + createData.Add(jsonutils.NewString(contactType), "type") + _, err := self.Create(ctx, jsonutils.JSONNull, createData, nil) + if err != nil { + return errors.Wrapf(err, "Create config (%s, %s, %s) failed", contactType, key, tmp) + } + } + return nil +} + +// CreateNotification create new notifications and send them through rpc.RpcService. +// If data contains 'gid' field, that means that send message to all users in group. +// Else send messager to user whose uid equals 'uid' in data. +func (self *NotifyModelDispatcher) CreateNotification(ctx context.Context, data jsonutils.JSONObject) (jsonutils.JSONObject, error) { + // Get all contacts info of group if data contains "gid". + // If no contact, return ErrContactNotFound. + contactType, _ := data.GetString("contact_type") + group, id := false, "" + if data.Contains("gid") { + group = true + id, _ = data.GetString("gid") + } else { + id, _ = data.GetString("uid") + } + contacts, err := models.ContactManager.GetAllNotify(id, contactType, group) + if err != nil { + return nil, httperrors.NewGeneralError(errors.Wrap(err, "get all contacts error")) + } + + notificationIDs, err := models.NotificationManager.BatchCreate(ctx, data, contacts) + if err != nil { + return nil, httperrors.NewGeneralError(err) + } + ret := jsonutils.NewDict() + ret.Add(jsonutils.NewStringArray(notificationIDs), "notifications") + return ret, nil +} + +// Verify process: +// 1.fetch verify by ID; 2.check that if verify is expired; +// 3.if not check that if token is correct and update status of contact whose id is verify's CID +// 4.otherwise generate a new verify and delete old one +func (self *NotifyModelDispatcher) Verify(ctx context.Context, params map[string]string, query jsonutils.JSONObject) error { + processID := params[""] + token, _ := query.GetString("token") + manager := models.VerifyManager + verifys, err := manager.FetchByID(processID) + if err != nil { + return httperrors.NewGeneralError(err) + } + var verifition models.SVerify + if len(verifys) == 0 { + return httperrors.NewNotFoundError("%s verify record not found", processID) + } + current, have := time.Now(), false + for i := range verifys { + if current.Before(verifys[i].ExpireAt) { + verifition = verifys[i] + have = true + break + } + } + if !have { + return httperrors.NewBadRequestError(models.VERIFICATION_TOKEN_EXPIRED) + } + if verifition.Token != token { + return httperrors.NewBadRequestError(models.VERIFICATION_TOKEN_INVALID) + } + // modify contact's status and verified time. + data := jsonutils.NewDict() + data.Set("status", jsonutils.NewString(models.CONTACT_VERIFIED)) + data.Set("verified_at", jsonutils.NewTimeString(current)) + _, err = self.Update(ctx, verifition.CID, jsonutils.JSONNull, data, nil) + if err != nil { + return httperrors.NewGeneralError(err) + } + return nil +} + +// VerifyTrigger process: +// 1.fetch contact by the information in data +// 2.if contact'status is 'init', make a new verify and send a verify message to the contact adress +// 3.if contact'status is 'verifying', fetch verify by CID, generate a new verify if it has expired +// or return a error mention that "please don't try again". +func (self *NotifyModelDispatcher) VerifyTrigger(ctx context.Context, params map[string]string, data jsonutils.JSONObject) (jsonutils.JSONObject, error) { + uid := params[""] + contact, _ := data.GetString("contact") + contactType, _ := data.GetString("contact_type") + contacts, err := models.ContactManager.FetchByMore(uid, contact, contactType) + if err != nil { + return nil, errors.Error(fmt.Sprintf(`uid %q don't have contact %q of contact_type %q'`, uid, contact, contactType)) + } + userCred := policy.FetchUserCredential(ctx) + scontact := contacts[0] + + makeNewVerify := func() (jsonutils.JSONObject, error) { + verification := models.NewSVerify(contactType, scontact.ID) + err = models.VerifyManager.Create(ctx, userCred, verification) + if err != nil { + return nil, httperrors.NewGeneralError(err) + } + // update contact state + updateDate := jsonutils.NewDict() + updateDate.Set("status", jsonutils.NewString(models.CONTACT_VERIFYING)) + err = UpdateItem(models.ContactManager, &scontact, ctx, userCred, jsonutils.JSONNull, updateDate) + if err != nil { + return nil, httperrors.NewGeneralError(err) + } + processID := verification.ID + go models.SendVerifyMessage(processID, uid, contactType, contact, verification.Token) + ret := map[string]map[string]string{ + "contact": { + "process_id": processID, + }, + } + return jsonutils.Marshal(ret), nil + } + if scontact.Status == models.CONTACT_INIT { + return makeNewVerify() + } + if scontact.Status == models.CONTACT_VERIFYING { + if err != nil { + return nil, errors.Error(fmt.Sprintf(`uid %q don't have contact %q of contact_type %q`, uid, contact, contactType)) + } + verifications, err := models.VerifyManager.FetchByCID(scontact.ID) + if err != nil { + return nil, httperrors.NewGeneralError(err) + } + current := time.Now() + for _, verification := range verifications { + if current.After(verification.ExpireAt) { + //delete old one + err = DeleteItem(models.VerifyManager, &verification, ctx, userCred, jsonutils.JSONNull, jsonutils.JSONNull) + if err != nil { + return nil, httperrors.NewGeneralError(err) + } + return makeNewVerify() + } + } + return nil, httperrors.NewGeneralError(models.ErrVeritying) + } + + return jsonutils.JSONNull, nil +} + +// DeleteContacts delete a group of contacts +func (self *NotifyModelDispatcher) DeleteContacts(ctx context.Context, uids2 []jsonutils.JSONObject) error { + // Get all id of uid + uids := make([]string, len(uids2)) + for i := range uids2 { + uids[i] = strings.Trim(uids2[i].String(), `"`) + } + + contacts, err := models.ContactManager.FetchByUIDs(uids) + if err != nil { + return httperrors.NewGeneralError(err) + } + userCred := policy.FetchUserCredential(ctx) + deleteFailed := make([]string, 0, 1) + for _, contact := range contacts { + err = DeleteItem(models.ContactManager, &contact, ctx, userCred, jsonutils.JSONNull, jsonutils.JSONNull) + if err != nil { + deleteFailed = append(deleteFailed, contact.ID) + } + } + if len(deleteFailed) != 0 { + errInfo := strings.Join(deleteFailed, ", ") + " ; these contact delete failed." + return errors.Error(errInfo) + } + return nil +} + +// UpdateContacts analysis the data, update corresponding contacts if they exist in the database or create new ones. +func (self *NotifyModelDispatcher) UpdateContacts(ctx context.Context, idstr string, query jsonutils.JSONObject, data jsonutils.JSONObject, ctxIds []dispatcher.SResourceContext) (jsonutils.JSONObject, error) { + datas, err := data.GetArray("contacts") + if err != nil { + return nil, httperrors.NewGeneralError(errors.Wrapf(err, `"contacts" not found`)) + } + type pair struct { + contact string + enabled string + } + + // contactInfos will be used to find all contact info need to update. + // And others will be created. + contactInfos := make(map[string]pair) + contactTypes := make([]string, len(datas)) + for i := range datas { + contactType, _ := datas[i].GetString("contact_type") + contact, _ := datas[i].GetString("contact") + enabled := "-1" + if datas[i].Contains("enabled") { + enabled, _ = datas[i].GetString("enabled") + } + contactInfos[contactType] = pair{contact, enabled} + contactTypes[i] = contactType + } + + records, err := models.ContactManager.FetchByUIDAndCType(idstr, contactTypes) + if err != nil { + return nil, httperrors.NewGeneralError(err) + } + + // updateFailed record the information of failed update + updateFailed := make([]string, 0, 1) + deleteFailed := make([]string, 0, 1) + // UpdateItem contact info + userCred := policy.FetchUserCredential(ctx) + for i := range records { + contactType := records[i].ContactType + pairUpdate := contactInfos[contactType] + if len(pairUpdate.contact) == 0 { + // delete + err = DeleteItem(models.ContactManager, &records[i], ctx, userCred, jsonutils.JSONNull, jsonutils.JSONNull) + if err != nil { + deleteFailed = append(deleteFailed, fmt.Sprintf(`uid:%q, contact_type:%q`, idstr, contactType)) + } + continue + } + updateData := jsonutils.NewDict() + updateData.Set("contact", jsonutils.NewString(pairUpdate.contact)) + if pairUpdate.enabled != "-1" { + updateData.Set("enabled", jsonutils.NewString(pairUpdate.enabled)) + } + updateData.Set("status", jsonutils.NewString("init")) + err = UpdateItem(models.ContactManager, &records[i], ctx, userCred, jsonutils.JSONNull, updateData) + if err != nil { + updateFailed = append(updateFailed, fmt.Sprintf(`uid:%q, contact_type:%q, contact:%q`, idstr, contactType, pairUpdate.contact)) + } + delete(contactInfos, contactType) + } + + // createFailed record the information of failed creation + createFailed := make([]string, 0, 1) + // Create contact info + newDatas := make([]map[string]interface{}, 0, len(contactInfos)) + for conType, conPair := range contactInfos { + tmpMap := map[string]interface{}{ + "uid": idstr, + "contact_type": conType, + "contact": conPair.contact, + } + if conPair.enabled != "-1" { + tmpMap["enabled"] = conPair.enabled + } + // dingtalk don't need verify, judge and specified status for now + if conType == "dingtalk" { + tmpMap["status"] = models.CONTACT_VERIFIED + tmpMap["verified_at"] = time.Now() + } + newDatas = append(newDatas, tmpMap) + } + + for _, newData := range newDatas { + _, err := self.Create(ctx, jsonutils.JSONNull, jsonutils.Marshal(newData), ctxIds) + if err != nil { + createFailed = append(createFailed, fmt.Sprintf(`uid:%q, contact:%q, contact_type:%q`, idstr, newData["contact_type"], newData["contact"])) + } + } + + // generate error through updateFailed and createFailed + if len(updateFailed) != 0 || len(createFailed) != 0 || len(deleteFailed) != 0 { + var errInfoBuffer bytes.Buffer + if len(updateFailed) != 0 { + errInfoBuffer.WriteString(strings.Join(updateFailed, "; ")) + errInfoBuffer.WriteString(" update failed. ") + } + if len(deleteFailed) != 0 { + errInfoBuffer.WriteString(strings.Join(updateFailed, "; ")) + errInfoBuffer.WriteString(" delete failed. ") + } + if len(createFailed) != 0 { + errInfoBuffer.WriteString(strings.Join(createFailed, "; ")) + errInfoBuffer.WriteString(" create failed. ") + } + errInfo := errInfoBuffer.String() + return nil, httperrors.NewGeneralError(errors.Error(errInfo)) + } + return data, nil +} + +// fetchEnv fetch handler, params, query and body from ctx(context.Context) +func fetchEnv(ctx context.Context, w http.ResponseWriter, r *http.Request) (*NotifyModelDispatcher, map[string]string, jsonutils.JSONObject, jsonutils.JSONObject) { + params, query, body := appsrv.FetchEnv(ctx, w, r) + metadata := appctx.AppContextMetadata(ctx) + manager, ok := metadata["manager"].(*NotifyModelDispatcher) + if !ok { + log.Fatalf("No manager found for URL: %s", r.URL) + } + return manager, params, query, body +} + +func mergeQueryParams(params map[string]string, query jsonutils.JSONObject, excludes ...string) jsonutils.JSONObject { + if query == nil { + query = jsonutils.NewDict() + } + queryDict := query.(*jsonutils.JSONDict) + for k, v := range params { + if !utils.IsInStringArray(k, excludes) { + queryDict.Add(jsonutils.NewString(v), k[1:len(k)-1]) + } + } + return queryDict +} + +// DeleteItem delete a database record corresponding to model +func DeleteItem(manager db.IModelManager, model db.IModel, ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) error { + err := model.ValidateDeleteCondition(ctx) + if err != nil { + log.Errorf("validate delete condition error: %s", err) + return err + } + err = model.CustomizeDelete(ctx, userCred, query, data) + if err != nil { + log.Errorf("customize delete error: %s", err) + return httperrors.NewNotAcceptableError(err.Error()) + } + model.PreDelete(ctx, userCred) + err = model.Delete(ctx, userCred) + if err != nil { + log.Errorf("Delete error %s", err) + return err + } + model.PostDelete(ctx, userCred) + return nil +} + +// UpdateItem update a database record corresponding to model whose update fields are in data +func UpdateItem(manager db.IModelManager, item db.IModel, ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) error { + var err error + + err = item.ValidateUpdateCondition(ctx) + + if err != nil { + log.Errorf("validate update condition error: %s", err) + return httperrors.NewGeneralError(err) + } + + dataDict, ok := data.(*jsonutils.JSONDict) + if !ok { + return httperrors.NewInternalServerError("Invalid data JSONObject") + } + + dataDict, err = item.ValidateUpdateData(ctx, userCred, query, dataDict) + if err != nil { + errMsg := fmt.Sprintf("validate update data error: %s", err) + log.Errorf(errMsg) + return httperrors.NewGeneralError(err) + } + + item.PreUpdate(ctx, userCred, query, dataDict) + + diff, err := db.Update(item, func() error { + filterData := dataDict.CopyIncludes(updateFields(manager, userCred)...) + err = filterData.Unmarshal(item) + if err != nil { + errMsg := fmt.Sprintf("unmarshal fail: %s", err) + log.Errorf(errMsg) + return httperrors.NewGeneralError(err) + } + return nil + }) + + if err != nil { + log.Errorf("save update error: %s", err) + return httperrors.NewGeneralError(err) + } + db.OpsLog.LogEvent(item, db.ACT_UPDATE, diff, userCred) + + item.PostUpdate(ctx, userCred, query, data) + + return nil + +} + +// get the field of model which is d +func updateFields(manager db.IModelManager, userCred mcclient.TokenCredential) []string { + ret := make([]string, 0) + for _, col := range manager.TableSpec().Columns() { + tags := col.Tags() + update := tags["update"] + if allowAction(manager, userCred, update, db.IsAllowUpdate) { + ret = append(ret, col.Name()) + } + } + return ret +} + +func allowAction(manager db.IResource, userCred mcclient.TokenCredential, action string, testfunc func(scope rbacutils.TRbacScope, userCred mcclient.TokenCredential, manager db.IResource) bool) bool { + if action == "user" { + return true + } + if action == "domain" && (testfunc(rbacutils.ScopeDomain, userCred, manager) || testfunc(rbacutils.ScopeSystem, userCred, manager)) { + return true + } + if action == "admin" && testfunc(rbacutils.ScopeSystem, userCred, manager) { + return true + } + return false +} diff --git a/pkg/notify/handlers.go b/pkg/notify/handlers.go new file mode 100644 index 0000000000..1205aea068 --- /dev/null +++ b/pkg/notify/handlers.go @@ -0,0 +1,349 @@ +// Copyright 2019 Yunion +// +// 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 notify + +import ( + "context" + "fmt" + "net/http" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + + "yunion.io/x/onecloud/pkg/appsrv" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/mcclient/modules" + "yunion.io/x/onecloud/pkg/notify/models" + "yunion.io/x/onecloud/pkg/notify/utils" +) + +func InitHandlers(app *appsrv.Application) { + db.RegisterModelManager(models.ContactManager) + db.RegisterModelManager(models.VerifyManager) + db.RegisterModelManager(models.NotificationManager) + db.RegisterModelManager(models.ConfigManager) + AddNotifyDispatcher("/api/v1/", app) +} + +func AddNotifyDispatcher(prefix string, app *appsrv.Application) { + var metadata map[string]interface{} + var tags map[string]string + + // Contact Handler + modelDispatcher := NewNotifyModelDispatcher(models.ContactManager) + metadata, tags = map[string]interface{}{"manager": modelDispatcher}, map[string]string{"resource": modelDispatcher.KeywordPlural()} + h := app.AddHandler2("POST", + fmt.Sprintf("%s/%s//update-contact", prefix, modelDispatcher.KeywordPlural()), + modelDispatcher.Filter(contactUpdateHandler), metadata, "contact_update", tags) + modelDispatcher.CustomizeHandlerInfo(h) + // List + h = app.AddHandler2("GET", + fmt.Sprintf("%s/%s", prefix, modelDispatcher.KeywordPlural()), + modelDispatcher.Filter(listManyHandler), metadata, "list_contacts", tags) + modelDispatcher.CustomizeHandlerInfo(h) + + h = app.AddHandler2("GET", + fmt.Sprintf("%s/%s/", prefix, modelDispatcher.KeywordPlural()), + modelDispatcher.Filter(listOneHandler), metadata, "list_by_uid", tags) + modelDispatcher.CustomizeHandlerInfo(h) + + h = app.AddHandler2("POST", + fmt.Sprintf("%s/%s/delete-contact", prefix, modelDispatcher.KeywordPlural()), + modelDispatcher.Filter(deleteContactHandler), metadata, "delete", tags) + modelDispatcher.CustomizeHandlerInfo(h) + + // verify-trigger + h = app.AddHandler2("POST", + fmt.Sprintf("%s/%s//verify", prefix, modelDispatcher.KeywordPlural()), + modelDispatcher.Filter(verifyTriggerHandler), metadata, "verify_trigger", tags) + modelDispatcher.CustomizeHandlerInfo(h) + + // Verify Handler, this modelDispatcher need db.DBModelDispatcher'Create function to create Contact so this modelDispatcher is + // NotifyModelDispatcher whose DBModelDispatcher has modelManager models.ContactManager + metadata, tags = map[string]interface{}{"manager": modelDispatcher}, map[string]string{"resource": models.VerifyManager.KeywordPlural()} + h = app.AddHandler2("GET", + fmt.Sprintf("%s/%s/", prefix, models.VerifyManager.KeywordPlural()), + modelDispatcher.Filter(verifyHandler), metadata, "verify", tags) + + // notification Handler + modelDispatcher = NewNotifyModelDispatcher(models.NotificationManager) + metadata, tags = map[string]interface{}{"manager": modelDispatcher}, map[string]string{"resource": modelDispatcher.KeywordPlural()} + h = app.AddHandler2("POST", + fmt.Sprintf("%s/%s/", prefix, modelDispatcher.KeywordPlural()), + modelDispatcher.Filter(notificationHandler), metadata, "send_notifications", tags) + modelDispatcher.CustomizeHandlerInfo(h) + h = app.AddHandler2("GET", + fmt.Sprintf("%s/%s/", prefix, modelDispatcher.KeywordPlural()), + modelDispatcher.Filter(listHandler), metadata, "send_notifications", tags) + modelDispatcher.CustomizeHandlerInfo(h) + h = app.AddHandler2("GET", + fmt.Sprintf("%s/%s/", prefix, modelDispatcher.KeywordPlural()), + modelDispatcher.Filter(listHandler), metadata, "list_notification_by_id", tags) + modelDispatcher.CustomizeHandlerInfo(h) + + // config Handler + modelDispatcher = NewNotifyModelDispatcher(models.ConfigManager) + metadata, tags = map[string]interface{}{"manager": modelDispatcher}, map[string]string{"resource": modelDispatcher.KeywordPlural()} + h = app.AddHandler2("POST", + fmt.Sprintf("%s/%s/", prefix, modelDispatcher.KeywordPlural()), + modelDispatcher.Filter(configUpdateHandler), metadata, "update_configs", tags) + modelDispatcher.CustomizeHandlerInfo(h) + h = app.AddHandler2("GET", + fmt.Sprintf("%s/%s/", prefix, modelDispatcher.KeywordPlural()), + modelDispatcher.Filter(configGetHandler), metadata, "get_configs", tags) + modelDispatcher.CustomizeHandlerInfo(h) + h = app.AddHandler2("DELETE", + fmt.Sprintf("%s/%s/", prefix, modelDispatcher.KeywordPlural()), + modelDispatcher.Filter(configDeleteHandler), metadata, "delete_configs", tags) + modelDispatcher.CustomizeHandlerInfo(h) + + // email handler for being compatible + h = app.AddHandler2("POST", + fmt.Sprintf("%s/%s/", prefix, EMAIL_KEYWORDPLURAL), + modelDispatcher.Filter(emailConfigUpdateHandler), metadata, "", tags) + modelDispatcher.CustomizeHandlerInfo(h) + h = app.AddHandler2("GET", + fmt.Sprintf("%s/%s/", prefix, EMAIL_KEYWORDPLURAL), + modelDispatcher.Filter(emailConfigGetHandler), metadata, "", tags) + modelDispatcher.CustomizeHandlerInfo(h) + h = app.AddHandler2("DELETE", + fmt.Sprintf("%s/%s/", prefix, EMAIL_KEYWORDPLURAL), + modelDispatcher.Filter(emailConfigDeleteHandler), metadata, "", tags) + modelDispatcher.CustomizeHandlerInfo(h) + h = app.AddHandler2("PUT", + fmt.Sprintf("%s/%s/", prefix, EMAIL_KEYWORDPLURAL), + modelDispatcher.Filter(emailConfigUpdateHandler), metadata, "", tags) + modelDispatcher.CustomizeHandlerInfo(h) + + h = app.AddHandler2("POST", + fmt.Sprintf("%s/%s/", prefix, SMS_KEYWORDPLURAL), + modelDispatcher.Filter(smsConfigUpdateHandler), metadata, "", tags) + modelDispatcher.CustomizeHandlerInfo(h) + h = app.AddHandler2("GET", + fmt.Sprintf("%s/%s/", prefix, SMS_KEYWORDPLURAL), + modelDispatcher.Filter(smsConfigGetHandler), metadata, "", tags) + modelDispatcher.CustomizeHandlerInfo(h) + h = app.AddHandler2("DELETE", + fmt.Sprintf("%s/%s/", prefix, SMS_KEYWORDPLURAL), + modelDispatcher.Filter(smsConfigDeleteHandler), metadata, "", tags) + modelDispatcher.CustomizeHandlerInfo(h) + h = app.AddHandler2("PUT", + fmt.Sprintf("%s/%s/", prefix, SMS_KEYWORDPLURAL), + modelDispatcher.Filter(smsConfigUpdateHandler), metadata, "", tags) + modelDispatcher.CustomizeHandlerInfo(h) + +} + +func configDeleteHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + manager, params, _, _ := fetchEnv(ctx, w, r) + err := manager.DeleteConfig(ctx, params) + if err != nil { + httperrors.GeneralServerError(w, err) + } +} + +func configGetHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + manager, params, query, _ := fetchEnv(ctx, w, r) + ret, err := manager.GetConfig(ctx, params, query) + if err != nil { + httperrors.GeneralServerError(w, err) + } + appsrv.SendJSON(w, ret) +} + +func configUpdateHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + manager, _, _, body := fetchEnv(ctx, w, r) + if body.Contains("config") { + body, _ = body.Get("config") + } + if body.Contains("configs") { + body, _ = body.Get("config") + } + err := manager.UpdateConfig(ctx, body) + if err != nil { + httperrors.GeneralServerError(w, err) + } +} + +func notificationHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + manager, _, _, body := fetchEnv(ctx, w, r) + data, err := body.Get(manager.Keyword()) + if err != nil { + httperrors.BadRequestError(w, "request body should contain %s", manager.Keyword()) + } + ret, err := manager.CreateNotification(ctx, data) + if err != nil { + httperrors.GeneralServerError(w, err) + return + } + appsrv.SendJSON(w, ret) +} + +// verify handler +func verifyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + manager, params, query, _ := fetchEnv(ctx, w, r) + err := manager.Verify(ctx, params, query) + if err != nil { + httperrors.GeneralServerError(w, err) + } +} + +// contact update handler +func contactUpdateHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + manager, params, _, body := fetchEnv(ctx, w, r) + + var data jsonutils.JSONObject + if body != nil { + if body.Contains(manager.Keyword()) { + data, _ = body.Get(manager.Keyword()) + if data == nil { + data = body.(*jsonutils.JSONDict) + } + } else { + data = body + } + } else { + data = jsonutils.NewDict() + } + + // check that if the uid is exist + uid := params[""] + _, err := utils.GetUserByID(uid) + if err != nil { + log.Errorf(`uid %q not found`, uid) + httperrors.NotFoundError(w, "Uid Not Found") + return + } + _, err = manager.UpdateContacts(ctx, uid, jsonutils.JSONNull, data, nil) + if err != nil { + log.Errorf(err.Error()) + httperrors.BadRequestError(w, "") + return + } + return +} + +// delete contact handler +func deleteContactHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + manager, _, _, body := fetchEnv(ctx, w, r) + + var data []jsonutils.JSONObject + var err error + if body != nil { + data, err = body.GetArray(manager.KeywordPlural()) + if err != nil { + httperrors.BadRequestError(w, "request body should have %s", manager.KeywordPlural()) + } + } + err = manager.DeleteContacts(ctx, data) + + if err != nil { + log.Errorf("delete contact of %s failed, error: %s", data, err) + httperrors.GeneralServerError(w, errors.Error("delete failed")) + } +} + +// verify trigger handler +func verifyTriggerHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + manager, params, _, body := fetchEnv(ctx, w, r) + data, err := body.Get(models.ContactManager.Keyword()) + if err != nil { + httperrors.BadRequestError(w, "request body should have %s", manager.KeywordPlural()) + } + ret, err := manager.VerifyTrigger(ctx, params, data) + if err != nil { + log.Errorf("verifyTrigger failed beacause %s", err) + httperrors.GeneralServerError(w, err) + } + appsrv.SendJSON(w, ret) +} + +//speciallist hander for contact records +func listManyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + manager, params, query, _ := fetchEnv(ctx, w, r) + listResult, err := manager.List(ctx, mergeQueryParams(params, query), nil) + if err != nil { + httperrors.GeneralServerError(w, err) + return + } + listResult = arrangeList(listResult) + appsrv.SendJSON(w, modules.ListResult2JSONWithKey(listResult, manager.KeywordPlural())) +} + +// list handler for all resource in notify module +func listHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + manager, params, query, _ := fetchEnv(ctx, w, r) + listResult, err := manager.List(ctx, mergeQueryParams(params, query), nil) + if err != nil { + httperrors.GeneralServerError(w, err) + return + } + appsrv.SendJSON(w, modules.ListResult2JSONWithKey(listResult, manager.KeywordPlural())) +} + +func listOneHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + manager, params, query, _ := fetchEnv(ctx, w, r) + listResult, err := manager.List(ctx, mergeQueryParams(params, query), nil) + if err != nil { + httperrors.GeneralServerError(w, err) + return + } + appsrv.SendJSON(w, wrap(arrangeOne(listResult), manager.Keyword())) +} + +func wrap(data jsonutils.JSONObject, key string) jsonutils.JSONObject { + ret := jsonutils.NewDict() + ret.Add(data, key) + return ret +} + +// For limit option, there is a bug but don't fix it for now. +// This limit point to contact record, but these contact records whose uid are same +// are considered as one record. +func arrangeList(listResult *modules.ListResult) *modules.ListResult { + ret := make(map[string]*jsonutils.JSONArray) + for _, data := range listResult.Data { + uid, _ := data.GetString("uid") + _, ok := ret[uid] + if !ok { + ret[uid] = jsonutils.NewArray() + } + ret[uid].Add(data) + } + data := make([]jsonutils.JSONObject, len(ret)) + index := 0 + for uid, value := range ret { + cr := models.NewSContactResponse(uid, value.String()) + data[index] = jsonutils.Marshal(cr) + index++ + } + listResult.Data = data + listResult.Total = len(ret) + return listResult +} + +func arrangeOne(listResult *modules.ListResult) jsonutils.JSONObject { + if len(listResult.Data) == 0 { + return jsonutils.NewDict() + } + uid, _ := listResult.Data[0].GetString("uid") + details := jsonutils.NewArray() + for _, data := range listResult.Data { + details.Add(data) + } + return jsonutils.Marshal(models.NewSContactResponse(uid, details.String())) +} diff --git a/pkg/notify/models/base.go b/pkg/notify/models/base.go new file mode 100644 index 0000000000..80e0d3c16c --- /dev/null +++ b/pkg/notify/models/base.go @@ -0,0 +1,90 @@ +// Copyright 2019 Yunion +// +// 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 models + +import ( + "context" + "fmt" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/mcclient" +) + +type SResourceBase struct { + db.SResourceBase + + CreateBy string `width:"128" charset:"ascii" nullable:"true" create:"optional"` + UpdateBy string `width:"128" charset:"ascii" nullable:"true" update:"user"` + DeleteBy string `width:"128" charset:"ascii" nullable:"true"` + + Remark jsonutils.JSONObject `get:"user"` +} + +type SResourceBaseManager struct { + db.SResourceBaseManager +} + +type IResourceBaseModel interface { + db.IModel + GetIResourceBaseModel() IResourceBaseModel + SetDeleteBy(string) +} + +func NewResourceBaseManager(dt interface{}, tableName string, keyword string, keywordPlural string) SResourceBaseManager { + return SResourceBaseManager{db.NewResourceBaseManager(dt, tableName, keyword, keywordPlural)} +} + +func (self *SResourceBase) GetIResourceBaseModel() IResourceBaseModel { + return self.GetVirtualObject().(IResourceBaseModel) +} + +func (self *SResourceBase) SetDeleteBy(uid string) { + self.DeleteBy = uid +} + +func (self *SResourceBaseManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data *jsonutils.JSONDict) (*jsonutils.JSONDict, error) { + data, err := self.SResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, data) + if err != nil { + return nil, err + } + data.Set("create_by", jsonutils.NewString(userCred.GetUserId())) + return data, nil +} + +func (self *SResourceBase) PreUpdate(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) { + self.SResourceBase.PreUpdate(ctx, userCred, query, data) + self.UpdateBy = userCred.GetUserId() +} + +func (self *SResourceBase) Delete(ctx context.Context, userCred mcclient.TokenCredential) error { + item := self.GetIResourceBaseModel() + _, err := db.Update(item, func() error { + item.SetDeleteBy(userCred.GetUserId()) + return item.MarkDelete() + }) + if err != nil { + msg := fmt.Sprintf("save update error %s", err) + log.Errorf(msg) + return httperrors.NewGeneralError(err) + } + if userCred != nil { + db.OpsLog.LogEvent(self, db.ACT_DELETE, self.GetShortDesc(ctx), userCred) + } + return nil +} diff --git a/pkg/notify/models/consts.go b/pkg/notify/models/consts.go new file mode 100644 index 0000000000..afc62c88de --- /dev/null +++ b/pkg/notify/models/consts.go @@ -0,0 +1,46 @@ +// Copyright 2019 Yunion +// +// 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 models + +const ( + EMAIL = "email" + MOBILE = "mobile" + DINGTALK = "dingtalk" + WEBCONSOLE = "webconsole" + // Received a task about sending a notification + NOTIFY_RECEIVED = "received" + // Nofity module hasn't sent the notification + NOTIFY_UNSENT = "unsent" + // Nofity module has sent notification, but result unkown + NOTIFY_SENT = "sent" + // Notification was sent successfully + NOTIFY_OK = "sent_ok" + // That sent a notification is failed + NOTIFY_FAIL = "sent_fail" + // Contact's status is init which means no verifying + CONTACT_INIT = "init" + // Contact's status is verifying + CONTACT_VERIFYING = "verifying" + // Contact's status is verified + CONTACT_VERIFIED = "verified" + // Verification was sent + VERIFICATION_SENT = "sent" + // Verification was verified + VERIFICATION_VERIFIED = "verified" + + VERIFICATION_TOKEN_EXPIRED = "Verification code expired" + + VERIFICATION_TOKEN_INVALID = "Incorrect verification code" +) diff --git a/pkg/notify/models/errors.go b/pkg/notify/models/errors.go new file mode 100644 index 0000000000..36e9186fb6 --- /dev/null +++ b/pkg/notify/models/errors.go @@ -0,0 +1,26 @@ +// Copyright 2019 Yunion +// +// 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 models + +import "errors" + +var ( + ErrContactNotFound = errors.New("Contact Not Found") + ErrVeritying = errors.New("During the verification process, please do not try again") + + ErrDial = errors.New("Dial Failed") + ErrGetConfig = errors.New("Get Config Failed") + ErrUpdateConfig = errors.New("Update Config Failed") +) diff --git a/pkg/notify/models/initdb.go b/pkg/notify/models/initdb.go new file mode 100644 index 0000000000..f14d07b4e4 --- /dev/null +++ b/pkg/notify/models/initdb.go @@ -0,0 +1,27 @@ +package models + +import ( + "yunion.io/x/log" + "yunion.io/x/onecloud/pkg/cloudcommon/db" +) + +func InitDB() error { + for _, manager := range []db.IModelManager{ + /* + * Important!!! + * initialization order matters, do not change the order + */ + + ContactManager, + VerifyManager, + NotificationManager, + ConfigManager, + } { + err := manager.InitializeData() + if err != nil { + log.Errorf("Manager %s initializeData fail %s", manager.Keyword(), err) + // return err skip error table + } + } + return nil +} diff --git a/pkg/notify/models/mod_config.go b/pkg/notify/models/mod_config.go new file mode 100644 index 0000000000..831c002ddd --- /dev/null +++ b/pkg/notify/models/mod_config.go @@ -0,0 +1,119 @@ +// Copyright 2019 Yunion +// +// 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 models + +import ( + "context" + "fmt" + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + "yunion.io/x/sqlchemy" + + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/mcclient" +) + +type SConfigManager struct { + SStatusStandaloneResourceBaseManager +} + +var ConfigManager *SConfigManager + +func init() { + ConfigManager = &SConfigManager{ + SStatusStandaloneResourceBaseManager: NewStatusStandaloneResourceBaseManager( + SConfig{}, + "notify_t_config", + "config", + "configs", + ), + } + ConfigManager.SetVirtualObject(ConfigManager) +} + +// SConfig is a table which storage (k,v) and its type. +// The three important concepts are key, value and type. +// Key and type uniquely identify a value. +type SConfig struct { + SStatusStandaloneResourceBase + + Type string `width:"15" nullable:"false" create:"required" list:"user""` + KeyText string `width:"30" nullable:"false" create:"required" list:"user"` + ValueText string `width:"100" nullable:"false" create:"required" list:"user"` +} + +// ListItemFilter is a hook function belong to IModelManager interface when Listing. +// This will Called in yunion.io/x/onecloud/pkg/cloudcommon/db.List function. +func (self *SConfigManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (*sqlchemy.SQuery, error) { + if !query.Contains("type") { + return q, nil + } + contactType, _ := query.GetString("type") + q.Filter(sqlchemy.Equals(q.Field("type"), contactType)) + return q, nil +} + +// GetValue fetch the SConfig struct corresponding to key and type. +func (self *SConfigManager) GetValue(key, contactType string) (*SConfig, error) { + q := self.Query() + q.Filter(sqlchemy.AND(sqlchemy.Equals(q.Field("type"), contactType), sqlchemy.Equals(q.Field("key_text"), key))) + configs := make([]SConfig, 0, 1) + err := db.FetchModelObjects(self, q, &configs) + if err != nil { + return nil, errors.Wrap(err, "Fetch SConfig by key and type failed") + } + if len(configs) == 0 { + return nil, errors.Error("There is no SConfig whose key and type meet the requirement") + } + return &configs[0], nil +} + +// Get all (k, v) whose type is contactType. +func (self *SConfigManager) GetVauleByType(contactType string) (map[string]string, error) { + configs, err := self.GetConfigByType(contactType) + if err != nil { + return nil, err + } + ret := make(map[string]string) + for i := range configs { + ret[configs[i].KeyText] = configs[i].ValueText + } + return ret, nil +} + +func (self *SConfigManager) InitializeData() error { + sql := fmt.Sprintf("update %s set updated_at=gmt_modified, deleted=is_deleted, created_at=gmt_create, deleted_at=gmt_deleted, update_by=modified_by, delete_by=deleted_by", self.TableSpec().Name()) + q := sqlchemy.NewRawQuery(sql, "") + q.Row() + sql = fmt.Sprintf("update %s set type='mobile' where type='sms_aliyun'", self.TableSpec().Name()) + q = sqlchemy.NewRawQuery(sql, "") + q.Row() + return nil +} + +// Fetch all SConfig struct which type is contactType. +func (self *SConfigManager) GetConfigByType(contactType string) ([]SConfig, error) { + q := self.Query() + q.Filter(sqlchemy.Equals(q.Field("type"), contactType)) + configs := make([]SConfig, 0, 5) + err := db.FetchModelObjects(self, q, &configs) + if err != nil { + return nil, errors.Wrap(err, "Fetch SConfigs by type failed") + } + //if len(configs) == 0 { + // return nil, errors.Error("There is no SConfig whose type meet the requirement") + //} + return configs, nil +} diff --git a/pkg/notify/models/mod_contact.go b/pkg/notify/models/mod_contact.go new file mode 100644 index 0000000000..20f4c8ad0d --- /dev/null +++ b/pkg/notify/models/mod_contact.go @@ -0,0 +1,166 @@ +// Copyright 2019 Yunion +// +// 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 models + +import ( + "context" + "fmt" + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/sqlchemy" + + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/notify/utils" +) + +type SContactManager struct { + SStatusStandaloneResourceBaseManager +} + +var ContactManager *SContactManager + +func init() { + ContactManager = &SContactManager{ + SStatusStandaloneResourceBaseManager: NewStatusStandaloneResourceBaseManager( + SContact{}, + "notify_t_contacts", + "contact", + "contacts", + ), + } + ContactManager.SetVirtualObject(ContactManager) +} + +type SContact struct { + SStatusStandaloneResourceBase + + UID string `width:"128" nullable:"false" create:"required" list:"user" update:"user"` + ContactType string `width:"16" nullable:"false" create:"required" list:"user" update:"user"` + Contact string `width:"64" nullable:"false" create:"required" list:"user" update:"user"` + Enabled string `width:"5" nullable:"false" default:"1" create:"optional" list:"user" update:"user"` + VerifiedAt time.Time `update:"user" list:"user"` +} + +func (self *SContactManager) AllowListItems(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) bool { + return db.IsAdminAllowList(userCred, self) +} + +func (self *SContactManager) AllowCreateItem(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool { + return true +} + +func (self *SContactManager) InitializeData() error { + sql := fmt.Sprintf("update %s set updated_at=update_at, deleted=is_deleted", self.TableSpec().Name()) + q := sqlchemy.NewRawQuery(sql, "") + q.Row() + return nil +} + +func (self *SContactManager) FetchByUIDs(uids []string) ([]SContact, error) { + q := self.Query() + q = q.Filter(sqlchemy.In(q.Field("uid"), uids)) + records := make([]SContact, 0, len(uids)) + err := db.FetchModelObjects(self, q, &records) + if err != nil { + return nil, err + } + return records, nil +} + +func (self *SContactManager) FetchByUIDAndCType(uid string, contactTypes []string) ([]SContact, error) { + q := self.Query("id", "uid", "contact_type", "contact", "enabled") + q = q.Filter(sqlchemy.AND(sqlchemy.Equals(q.Field("uid"), uid), sqlchemy.In(q.Field("contact_type"), contactTypes))) + records := make([]SContact, 0, len(contactTypes)) + err := db.FetchModelObjects(self, q, &records) + if err != nil { + return nil, err + } + return records, nil +} + +func (self *SContactManager) FetchByMore(uid, contact, contactType string) ([]SContact, error) { + q := self.Query() + q.Filter(sqlchemy.AND(sqlchemy.Equals(q.Field("uid"), uid), sqlchemy.Equals(q.Field("contact"), contact), sqlchemy.Equals(q.Field("contact_type"), contactType))) + records := make([]SContact, 0, 1) + err := db.FetchModelObjects(self, q, &records) + if err != nil { + return nil, err + } + return records, nil +} + +func (self *SContactManager) FetchDingtalkContacts(uid string) { + // todo +} + +func (self *SContactManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (*sqlchemy.SQuery, error) { + queryDict := query.(*jsonutils.JSONDict) + if queryDict.Contains("uid") { + uid, _ := queryDict.GetString("uid") + q = q.Filter(sqlchemy.Equals(q.Field("uid"), uid)) + } + return q, nil +} + +func (self *SContactManager) GetAllNotify(id, contactType string, group bool) ([]SContact, error) { + var uids []string + var err error + + q := self.Query() + if !group { + q.Filter(sqlchemy.AND(sqlchemy.Equals(q.Field("uid"), id), sqlchemy.Equals(q.Field("contact_type"), contactType), sqlchemy.Equals(q.Field("status"), CONTACT_VERIFIED))) + uids = []string{id} + } else { + uids, err = utils.GetUsersByGroupID(id) + if err != nil { + return nil, err + } + q.Filter(sqlchemy.AND(sqlchemy.In(q.Field("uid"), uids), sqlchemy.Equals(q.Field("contact_type"), contactType))) + } + if contactType == WEBCONSOLE { + ret := make([]SContact, len(uids)) + for i := range uids { + ret[i] = SContact{ + UID: uids[i], + ContactType: WEBCONSOLE, + Contact: uids[i], + } + } + return ret, nil + } + contacts := make([]SContact, 0, 2) + err = db.FetchModelObjects(self, q, &contacts) + if err != nil { + return nil, err + } + return contacts, nil +} + +type SContactResponse struct { + Id string + Name string + Details string +} + +func NewSContactResponse(uid string, details string) SContactResponse { + name, _ := utils.GetUsernameByID(uid) + return SContactResponse{ + Id: uid, + Name: name, + Details: details, + } +} diff --git a/pkg/notify/models/mod_notification.go b/pkg/notify/models/mod_notification.go new file mode 100644 index 0000000000..f5734a854e --- /dev/null +++ b/pkg/notify/models/mod_notification.go @@ -0,0 +1,247 @@ +// Copyright 2019 Yunion +// +// 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 models + +import ( + "bytes" + "context" + "fmt" + "sync" + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + "yunion.io/x/sqlchemy" + + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/policy" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/notify/utils" +) + +type SNotificationManager struct { + SStatusStandaloneResourceBaseManager +} + +var NotificationManager *SNotificationManager + +func init() { + NotificationManager = &SNotificationManager{ + SStatusStandaloneResourceBaseManager: NewStatusStandaloneResourceBaseManager( + SNotification{}, + "notify_t_notification", + "notification", + "notifications", + ), + } + NotificationManager.SetVirtualObject(NotificationManager) +} + +type SNotification struct { + SStatusStandaloneResourceBase + + UID string `width:"128" nullable:"false" create:"required"` + ContactType string `width:"16" nullable:"false" create:"required"` + Topic string `width:"128" nullable:"false" create:"optional"` + Priority string `width:"16" nullable:"false" create:"optional"` + Msg string `create:"required"` + ReceivedAt time.Time `nullable:"false"` + SendAt time.Time `nullable:"false"` + SendBy string `width:"128" nullable:"false"` +} + +func (self *SNotificationManager) AllowListItems(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) bool { + return db.IsAdminAllowList(userCred, self) +} + +func (self *SNotificationManager) AllowCreateItem(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool { + return db.IsAdminAllowCreate(userCred, self) +} + +func (self *SNotificationManager) InitializeData() error { + sql := fmt.Sprintf("update %s set updated_at=update_at, deleted=is_deleted", self.TableSpec().Name()) + q := sqlchemy.NewRawQuery(sql, "") + q.Row() + return nil +} + +func (self *SNotificationManager) BatchCreate(ctx context.Context, data jsonutils.JSONObject, contacts []SContact) ([]string, error) { + userCred := policy.FetchUserCredential(ctx) + ownerID, err := utils.FetchOwnerId(ctx, NotificationManager, userCred, jsonutils.JSONNull) + if err != nil { + return nil, httperrors.NewGeneralError(err) + } + msg, _ := data.GetString("msg") + priority, _ := data.GetString("priority") + topic, _ := data.GetString("topic") + createFailed, createSuccess, contactSuccess := make([]string, 0), make([]*SNotification, 0, len(contacts)/2), make([]string, 0, len(contacts)/2) + + for i := range contacts { + createData := map[string]string{ + "uid": contacts[i].ID, + "contact_type": contacts[i].ContactType, + "topic": topic, + "priority": priority, + "msg": msg, + "send_by": userCred.GetUserId(), + "status": NOTIFY_UNSENT, + } + model, err := db.DoCreate(self, ctx, userCred, jsonutils.JSONNull, jsonutils.Marshal(createData), ownerID) + if err != nil { + createFailed = append(createFailed, contacts[i].ID) + } else { + createSuccess = append(createSuccess, model.(*SNotification)) + contactSuccess = append(contactSuccess, contacts[i].Contact) + } + } + go send(createSuccess, userCred, contactSuccess) + if len(createFailed) != 0 { + errInfo := new(bytes.Buffer) + errInfo.WriteString("notifications whose uid are ") + for i := range createFailed { + errInfo.WriteString(createFailed[i]) + errInfo.WriteString(", ") + } + errInfo.Truncate(errInfo.Len() - 2) + errInfo.WriteString("created failed.") + log.Errorf(errInfo.String()) + return nil, errors.Error("Not all notifications were sent successfully") + } + notificationIDs := make([]string, len(createSuccess)) + for i := range createSuccess { + notificationIDs[i] = createSuccess[i].ID + } + return notificationIDs, nil +} + +func (self *SNotificationManager) FetchNotOK(lastTime time.Time) ([]SNotification, error) { + q := self.Query() + q.Filter(sqlchemy.AND(sqlchemy.GE(q.Field("created_at"), lastTime), sqlchemy.NotEquals(q.Field("status"), NOTIFY_UNSENT))) + records := make([]SNotification, 0, 10) + err := db.FetchModelObjects(self, q, &records) + if err != nil { + return nil, err + } + return records, nil +} + +func send(notifications []*SNotification, userCred mcclient.TokenCredential, contacts []string) { + var wg sync.WaitGroup + sendone := func(notification *SNotification, contact string) { + err := notification.SetSentAndTime(userCred) + if err != nil { + log.Errorf("Change notification's status failed.") + return + } + err = RpcService.Send(notification.ContactType, contact, notification.Topic, notification.Msg, notification.Priority) + if err != nil { + log.Errorf("Send notification failed because that %s.", err.Error()) + notification.SetStatus(userCred, NOTIFY_FAIL, err.Error()) + } else { + notification.SetStatus(userCred, NOTIFY_OK, "") + } + wg.Done() + } + for i := range notifications { + wg.Add(1) + go sendone(notifications[i], contacts[i]) + } + wg.Wait() +} + +func (self *SNotification) SetSentAndTime(userCred mcclient.TokenCredential) error { + status := NOTIFY_SENT + if self.Status == status { + return nil + } + oldStatus := self.Status + _, err := db.Update(self, func() error { + self.Status = status + self.SendAt = time.Now() + return nil + }) + if err != nil { + return err + } + reason := "sent notification" + if userCred != nil { + notes := fmt.Sprintf("%s=>%s", oldStatus, status) + if len(reason) > 0 { + notes = fmt.Sprintf("%s: %s", notes, reason) + } + db.OpsLog.LogEvent(self, db.ACT_UPDATE_STATUS, notes, userCred) + } + return nil +} + +func (self *SNotification) SetStatusWithoutUserCred(status string) error { + _, err := db.Update(self, func() error { + self.Status = status + return nil + }) + if err != nil { + return err + } + return nil +} +func sendWithoutUserCred(notifications []SNotification) { + var wg sync.WaitGroup + sendone := func(notification SNotification) { + // Get contact + contact, err := ContactManager.FetchByUIDAndCType(notification.UID, []string{notification.ContactType}) + if err != nil { + return + } + if len(contact) == 0 { + return + } + // sent_at update todo + notification.SetStatusWithoutUserCred(NOTIFY_SENT) + err = RpcService.Send(notification.ContactType, contact[0].Contact, notification.Topic, notification.Msg, notification.Priority) + if err == nil { + return + } + if err != nil { + log.Errorf("Send notification failed because that %s.", err.Error()) + notification.SetStatusWithoutUserCred(NOTIFY_FAIL) + } else { + notification.SetStatusWithoutUserCred(NOTIFY_OK) + } + wg.Done() + } + for i := range notifications { + wg.Add(1) + go sendone(notifications[i]) + } + wg.Wait() +} + +func ReSend(minutes int) { + scope := time.Duration(minutes) * time.Minute + for { + select { + case <-time.After(scope / 2): + //lastTime := time.Now().Add(-scope) + //q := NotificationManager.Query() + notifications, err := NotificationManager.FetchNotOK(time.Now().Add(-scope)) + if err != nil { + break + } + sendWithoutUserCred(notifications) + } + } +} diff --git a/pkg/notify/models/mod_verify.go b/pkg/notify/models/mod_verify.go new file mode 100644 index 0000000000..b86c9e5769 --- /dev/null +++ b/pkg/notify/models/mod_verify.go @@ -0,0 +1,153 @@ +// Copyright 2019 Yunion +// +// 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 models + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/sqlchemy" + + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/notify/options" + "yunion.io/x/onecloud/pkg/notify/utils" +) + +type SVerifyManager struct { + SStatusStandaloneResourceBaseManager +} + +var VerifyManager *SVerifyManager + +func init() { + VerifyManager = &SVerifyManager{ + SStatusStandaloneResourceBaseManager: NewStatusStandaloneResourceBaseManager( + SVerify{}, + "notify_t_verify", + "verification", + "verifications", + ), + } + VerifyManager.SetVirtualObject(VerifyManager) +} + +type SVerify struct { + SStatusStandaloneResourceBase + + CID string `width:"128" nullable:"false" create:"required" list:"user"` + Token string `width:"200" nullable:"false" create:"required" list:"user"` + SendAt time.Time `nullable:"false" create:"optional"` + ExpireAt time.Time `create:"required" list:"user"` +} + +// NewSVerify Generate a SVerify instance which implement a Verification Token. +func NewSVerify(contactType string, cid string) *SVerify { + var token string + var expireAt time.Time + now := time.Now() + if contactType == EMAIL { + token = utils.GenerateEmailToken(32) + expireAt = now.Add(12 * time.Hour) + } else { + token = utils.GenerateMobileToken() + expireAt = now.Add(5 * time.Minute) + } + ret := &SVerify{ + CID: cid, + Token: token, + ExpireAt: expireAt, + SendAt: now, + } + ret.ID = DefaultUUIDGenerator() + return ret +} + +func (self *SVerifyManager) InitializeData() error { + sql := fmt.Sprintf("update %s set updated_at=update_at, deleted=is_deleted", self.TableSpec().Name()) + q := sqlchemy.NewRawQuery(sql, "") + q.Row() + return nil +} + +func (self *SVerifyManager) FetchByCID(cid string) ([]SVerify, error) { + q := self.Query() + q.Filter(sqlchemy.Equals(q.Field("cid"), cid)) + records := make([]SVerify, 0, 1) + err := db.FetchModelObjects(self, q, &records) + if err != nil { + return nil, err + } + return records, nil +} + +func (self *SVerifyManager) FetchByID(id string) ([]SVerify, error) { + q := self.Query() + q.Filter(sqlchemy.Equals(q.Field("id"), id)) + records := make([]SVerify, 0, 1) + err := db.FetchModelObjects(self, q, &records) + if err != nil { + return nil, err + } + return records, nil +} + +func (self *SVerifyManager) Create(ctx context.Context, userCred mcclient.TokenCredential, verify *SVerify) error { + data := jsonutils.Marshal(verify) + ownerID, err := utils.FetchOwnerId(ctx, self, userCred, data) + if err != nil { + return err + } + _, err = db.DoCreate(self, ctx, userCred, jsonutils.JSONNull, data, ownerID) + if err != nil { + return err + } + return nil +} + +func SendVerifyMessage(processId, uid, contactType, contact, token string) { + var err error + var msg string + if contactType == "email" { + emailUrl := strings.Replace(options.Options.VerifyEmailUrl, "{0}", processId, 1) + emailUrl = strings.Replace(emailUrl, "{1}", token, 1) + + // get uName + uName, err := utils.GetUsernameByID(uid) + if err != nil || len(uName) == 0 { + uName = "用户" + } + data := struct { + Name string + Link string + }{uName, emailUrl} + jsonStr, _ := json.Marshal(data) + msg = string(jsonStr) + } else if contactType == "mobile" { + msg = fmt.Sprintf(`{"code": "%s"}`, token) + } else { + //todo + } + + err = RpcService.Send(contactType, contact, "verify", msg, "") + if err != nil { + log.Errorf("Send verify message failed because that %s.", err.Error()) + } +} diff --git a/pkg/notify/models/send.go b/pkg/notify/models/send.go new file mode 100644 index 0000000000..0111dff007 --- /dev/null +++ b/pkg/notify/models/send.go @@ -0,0 +1,269 @@ +// Copyright 2019 Yunion +// +// 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 models + +import ( + "fmt" + "io/ioutil" + "net/rpc" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "yunion.io/x/log" + "yunion.io/x/pkg/errors" +) + +const ( + // ErrSendServiceNotFound means SRpcService's SendSerivces hasn't this Send Service. + ErrSendServiceNotFound = errors.Error("Send Service Not Found") + NOTINIT = "Send service hasn't been init" +) + +// RpcService is a single case of SRpcService +var RpcService *SRpcService + +// SRpcService provide rpc service about sending message for notify module and manage these services. +// SendServices storage all send service and its name. +// lock protect the SendServices. +type SRpcService struct { + SendServices map[string]*rpc.Client + socketFileDir string + lock sync.RWMutex +} + +// NewSRpcService create a SRpcService +func NewSRpcService(socketFileDir string) *SRpcService { + return &SRpcService{ + SendServices: make(map[string]*rpc.Client), + socketFileDir: socketFileDir, + } +} + +// InitAll init all Send Services, the init process is that: +// find all socket file in directory 'self.socketFileDir', if wrong return error; +// the name of file is the service's name; then try to dial to this rpc service +// through corresponding socket file, if failed, only print log but not return error. +func (self *SRpcService) InitAll() error { + files, err := ioutil.ReadDir(self.socketFileDir) + if err != nil { + return errors.Wrapf(err, "read dir %s failed", self.socketFileDir) + } + for _, file := range files { + filename := file.Name() + if !file.IsDir() && strings.Contains(filename, ".sock") { + serviceName := filename[:len(filename)-5] + self.checkAndAddOne(serviceName) + } + } + if len(self.SendServices) == 0 { + log.Errorf("No available send service.") + } else { + log.Infof("Total %d send service init successful", len(self.SendServices)) + } + return nil +} + +// UpdateServices will detect the self.sockFileDir every delay seconds. +// Add new service and delete disappeared one from self.SendServices. +func (self *SRpcService) UpdateServices(delay int) { + for { + select { + case <-time.After(time.Duration(delay) * time.Second): + err := self.updateService() + if err != nil { + log.Errorf("update services failed because that %s.", err.Error()) + } + } + } +} + +// StopAll stop all send service in self.SenderServices normally which can delete the socket file. +func (self *SRpcService) StopAll() { + for _, service := range self.SendServices { + service.Close() + } +} + +// Send call the corresponding rpc server.Send to send messager. +func (self *SRpcService) Send(contactType, contact, topic, msg, priority string) error { + self.lock.RLock() + sendService, ok := self.SendServices[contactType] + self.lock.RUnlock() + var err error + if !ok { + sendService, err = self.checkAndAddOne(contactType) + if err == ErrDial { + return ErrSendServiceNotFound + } + if err != nil { + return errors.Wrap(err, "Check or Add connection failed") + } + } + args := SSendArgs{ + Contact: contact, + Topic: topic, + Message: msg, + Priority: priority, + } + reply := SSendReply{} + err = sendService.Call("Server.Send", &args, &reply) + if err != nil { + // should check and send again. + // Possible situation: notify always keep connection but remote guy have restarted + // so that connection valid. + sendService, err = self.checkAndAddOne(contactType) + if err != nil { + return errors.Wrap(err, "Check or Add connection failed") + } + err = sendService.Call("Server.Send", &args, &reply) + if err != nil { + return errors.Wrap(err, "Send message failed.") + } + if !reply.Success { + return errors.Error(fmt.Sprintf("Send message failed because that %s.", reply.Msg)) + } + } + if !reply.Success { + if reply.Msg != NOTINIT { + return errors.Error(fmt.Sprintf("Send message failed because that %s.", reply.Msg)) + } + // should check and send again + sendService, err = self.checkAndAddOne(contactType) + if err != nil { + return errors.Wrap(err, "Check or Add connection failed") + } + err = sendService.Call("Server.Send", &args, &reply) + if err != nil { + return errors.Wrap(err, "Send message failed.") + } + if !reply.Success { + return errors.Error(fmt.Sprintf("Send message failed because that %s.", reply.Msg)) + } + } + return nil +} + +// RestartService can restart remote rpc server and pass config info. +// When first init notify Server, must Call this function. +// When accept the request about changing config, must Call this function. +func (self *SRpcService) RestartService(config map[string]string, serviceName string) { + self.lock.RLock() + sendService, ok := self.SendServices[serviceName] + self.lock.RUnlock() + var err error + if !ok { + sendService, err = self.checkAndAddOne(serviceName) + if err != nil { + log.Debugf("Restart Failed: %s", err.Error()) + return + } + } + args := SRestartArgs{Config: config} + + reply := SSendReply{} + err = sendService.Call("Server.UpdateConfig", &args, &reply) + if err != nil || !reply.Success { + log.Errorf("Restart rpc serve whose name is %s failed.", serviceName) + return + } +} + +// CheckAndAddOne check the status of service 'serviceName' +// If fail to dial to service, delete and remove sock file. +// if dial successfully, try to restart the service. +func (self *SRpcService) checkAndAddOne(serviceName string) (*rpc.Client, error) { + // Try to connect again + filename := filepath.Join(self.socketFileDir, serviceName+".sock") + rpcService, err := rpc.Dial("unix", filename) + if err != nil { + log.Debugf("Try to dial to service failed which unix socket file name is %s.", filename) + // This file maybe left behind inadvertently, so we should try to delete it + os.Remove(filename) + self.lock.Lock() + delete(self.SendServices, serviceName) + self.lock.Unlock() + return nil, ErrDial + } + + // GetKeyValue to config rpc Service + config, err := ConfigManager.GetVauleByType(serviceName) + if err != nil { + log.Debugf("Init service error which unix socket file name is %s because that get config about this failed", filename) + return nil, ErrGetConfig + } + args := SRestartArgs{config} + reply := SSendReply{} + rpcService.Call("Server.UpdateConfig", &args, &reply) + if !reply.Success { + log.Debugf("Init service error which unix socket file name is %s because that %s.", filename, reply.Msg) + return nil, ErrUpdateConfig + } + self.lock.Lock() + self.SendServices[serviceName] = rpcService + self.lock.Unlock() + return rpcService, nil +} + +func (self *SRpcService) updateService() error { + files, err := ioutil.ReadDir(self.socketFileDir) + if err != nil { + return errors.Wrapf(err, "read dir %s failed", self.socketFileDir) + } + original := make(map[string]*rpc.Client) + self.lock.RLock() + for serviceName, client := range self.SendServices { + original[serviceName] = client + } + self.lock.RUnlock() + for _, file := range files { + filename := file.Name() + if !file.IsDir() && strings.Contains(filename, ".sock") { + serviceName := filename[:len(filename)-5] + if _, ok := self.SendServices[serviceName]; ok { + delete(original, serviceName) + continue + } + self.checkAndAddOne(serviceName) + } + } + self.lock.Lock() + for serviceName := range original { + delete(self.SendServices, serviceName) + } + self.lock.Unlock() + for _, client := range original { + client.Close() + } + return nil +} + +type SSendArgs struct { + Contact string + Topic string + Message string + Priority string +} + +type SRestartArgs struct { + Config map[string]string +} + +type SSendReply struct { + Success bool + Msg string +} diff --git a/pkg/notify/models/standalone.go b/pkg/notify/models/standalone.go new file mode 100644 index 0000000000..270af9de5b --- /dev/null +++ b/pkg/notify/models/standalone.go @@ -0,0 +1,103 @@ +// Copyright 2019 Yunion +// +// 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 models + +import ( + "database/sql" + + "yunion.io/x/pkg/util/stringutils" + "yunion.io/x/sqlchemy" + + "yunion.io/x/onecloud/pkg/cloudcommon/db" +) + +type UUIDGenerator func() string + +var ( + DefaultUUIDGenerator = stringutils.UUID4 +) + +type SStandaloneResourceBase struct { + SResourceBase + + ID string `width:"128" charset:"ascii" primary:"true" list:"user" create:"optional"` +} + +func (model *SStandaloneResourceBase) BeforeInsert() { + if len(model.ID) == 0 { + model.ID = DefaultUUIDGenerator() + } +} + +type SStandaloneResourceBaseManager struct { + SResourceBaseManager +} + +func NewStandaloneResourceBaseManager(dt interface{}, tableName string, keyword string, keywordPlural string) SStandaloneResourceBaseManager { + return SStandaloneResourceBaseManager{NewResourceBaseManager(dt, tableName, keyword, keywordPlural)} +} + +func (manager *SStandaloneResourceBaseManager) GetIStandaloneModelManager() db.IStandaloneModelManager { + return manager.GetVirtualObject().(db.IStandaloneModelManager) +} + +func (manager *SStandaloneResourceBaseManager) FilterById(q *sqlchemy.SQuery, idStr string) *sqlchemy.SQuery { + return q.Equals("id", idStr) +} + +func (manager *SStandaloneResourceBaseManager) FilterByNotId(q *sqlchemy.SQuery, idStr string) *sqlchemy.SQuery { + return q.NotEquals("id", idStr) +} + +func (manager *SStandaloneResourceBaseManager) FetchById(idStr string) (db.IModel, error) { + return FetchById(manager.GetIStandaloneModelManager(), idStr) +} + +func FetchById(manager db.IModelManager, idStr string) (db.IModel, error) { + q := manager.Query() + q = manager.FilterById(q, idStr) + count, err := q.CountWithError() + if err != nil { + return nil, err + } + if count == 1 { + obj, err := db.NewModelObject(manager) + if err != nil { + return nil, err + } + err = q.First(obj) + if err != nil { + return nil, err + } else { + return obj, nil + } + } else if count > 1 { + return nil, sqlchemy.ErrDuplicateEntry + } else { + return nil, sql.ErrNoRows + } +} + +func (model *SStandaloneResourceBase) StandaloneModelManager() db.IStandaloneModelManager { + return model.GetModelManager().(db.IStandaloneModelManager) +} + +func (model *SStandaloneResourceBase) GetId() string { + return model.ID +} + +func (model *SStandaloneResourceBase) GetIStandaloneModel() db.IStandaloneModel { + return model.GetVirtualObject().(db.IStandaloneModel) +} diff --git a/pkg/notify/models/statusstandalone.go b/pkg/notify/models/statusstandalone.go new file mode 100644 index 0000000000..2c49b7781b --- /dev/null +++ b/pkg/notify/models/statusstandalone.go @@ -0,0 +1,91 @@ +// Copyright 2019 Yunion +// +// 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 models + +import ( + "context" + "fmt" + + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/utils" + + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/util/rbacutils" +) + +type SStatusStandaloneResourceBase struct { + SStandaloneResourceBase + + Status string `width:"36" charset:"ascii" nullable:"false" default:"init" list:"user" create:"optional" update:"user"` +} + +type SStatusStandaloneResourceBaseManager struct { + SStandaloneResourceBaseManager +} + +func NewStatusStandaloneResourceBaseManager(dt interface{}, tableName string, keyword string, keywordPlural string) SStatusStandaloneResourceBaseManager { + return SStatusStandaloneResourceBaseManager{SStandaloneResourceBaseManager: NewStandaloneResourceBaseManager(dt, tableName, keyword, keywordPlural)} +} + +func (model *SStatusStandaloneResourceBase) SetStatus(userCred mcclient.TokenCredential, status string, reason string) error { + if model.Status == status { + return nil + } + oldStatus := model.Status + _, err := db.Update(model, func() error { + model.Status = status + return nil + }) + if err != nil { + return err + } + if userCred != nil { + notes := fmt.Sprintf("%s=>%s", oldStatus, status) + if len(reason) > 0 { + notes = fmt.Sprintf("%s: %s", notes, reason) + } + db.OpsLog.LogEvent(model, db.ACT_UPDATE_STATUS, notes, userCred) + } + return nil +} + +func (model *SStatusStandaloneResourceBase) AllowPerformStatus(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool { + return db.IsAllowPerform(rbacutils.ScopeSystem, userCred, model, "status") +} + +func (model *SStatusStandaloneResourceBase) PerformStatus(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) { + status, err := data.GetString("status") + if err != nil { + return nil, err + } + reason, _ := data.GetString("reason") + err = model.SetStatus(userCred, status, reason) + return nil, err +} + +func (model *SStatusStandaloneResourceBase) IsInStatus(status ...string) bool { + return utils.IsInStringArray(model.Status, status) +} + +func (model *SStatusStandaloneResourceBase) AllowGetDetailsStatus(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) bool { + return db.IsAllowGetSpec(rbacutils.ScopeSystem, userCred, model, "status") +} + +func (model *SStatusStandaloneResourceBase) GetDetailsStatus(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (jsonutils.JSONObject, error) { + ret := jsonutils.NewDict() + ret.Add(jsonutils.NewString(model.Status), "status") + return ret, nil +} diff --git a/pkg/notify/options/options.go b/pkg/notify/options/options.go new file mode 100644 index 0000000000..e2cf8e8dfa --- /dev/null +++ b/pkg/notify/options/options.go @@ -0,0 +1,32 @@ +// Copyright 2019 Yunion +// +// 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 options + +import ( + "yunion.io/x/onecloud/pkg/cloudcommon/options" +) + +type NotifyOption struct { + options.CommonOptions + options.DBOptions + + DingtalkEnabled bool `help:"Enable dingtalk"` + SocketFileDir string `help:"Socket file directory" default:"/etc/yunion/notify"` + UpdateInterval int `help:"Update send services interval(unit:s)" default:30` + VerifyEmailUrl string + ReSendScope int `help:"Resend all messages that have not been sent successfully within ReSendScope minutes"` +} + +var Options NotifyOption diff --git a/pkg/notify/service.go b/pkg/notify/service.go new file mode 100644 index 0000000000..9aa05ba353 --- /dev/null +++ b/pkg/notify/service.go @@ -0,0 +1,66 @@ +// Copyright 2019 Yunion +// +// 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 notify + +import ( + "os" + + _ "github.com/go-sql-driver/mysql" + "yunion.io/x/log" + + "yunion.io/x/onecloud/pkg/cloudcommon" + "yunion.io/x/onecloud/pkg/cloudcommon/app" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + common_options "yunion.io/x/onecloud/pkg/cloudcommon/options" + "yunion.io/x/onecloud/pkg/notify/models" + "yunion.io/x/onecloud/pkg/notify/options" + "yunion.io/x/onecloud/pkg/notify/utils" +) + +func StartService() { + // parse options + opts := &options.Options + commonOpts := &options.Options.CommonOptions + dbOpts := &options.Options.DBOptions + baseOpts := &options.Options.BaseOptions + common_options.ParseOptions(opts, os.Args, "notify.conf", "notify") + + // init auth + app.InitAuth(commonOpts, func() { + log.Infof("Auth complete!") + }) + + // Session for user manager in keystone + utils.InitSession(commonOpts) + + // init handler + applicaion := app.InitApp(baseOpts, true) + InitHandlers(applicaion) + + // init database + db.EnsureAppInitSyncDB(applicaion, dbOpts, models.InitDB) + defer cloudcommon.CloseDB() + + // init rpc service + models.RpcService = models.NewSRpcService(opts.SocketFileDir) + models.RpcService.InitAll() + defer models.RpcService.StopAll() + go models.RpcService.UpdateServices(opts.UpdateInterval) + + // start ReSend service + go models.ReSend(opts.ReSendScope) + + app.ServeForever(applicaion, baseOpts) +} diff --git a/pkg/notify/utils/keystone.go b/pkg/notify/utils/keystone.go new file mode 100644 index 0000000000..ace6c416af --- /dev/null +++ b/pkg/notify/utils/keystone.go @@ -0,0 +1,59 @@ +// Copyright 2019 Yunion +// +// 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 utils + +import ( + "context" + + "yunion.io/x/jsonutils" + + "yunion.io/x/onecloud/pkg/cloudcommon/options" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/mcclient/auth" + "yunion.io/x/onecloud/pkg/mcclient/modules" +) + +var ( + session *mcclient.ClientSession +) + +func InitSession(options *options.CommonOptions) { + session = auth.GetAdminSession(context.Background(), options.Region, "v3") +} + +func GetUserByID(id string) (jsonutils.JSONObject, error) { + return modules.UsersV3.Get(session, id, jsonutils.NewDict()) +} + +func GetUsersByGroupID(gid string) ([]string, error) { + ret, err := modules.Groups.GetUsers(session, gid) + if err != nil { + return nil, err + } + ids := make([]string, len(ret.Data)) + for i := range ret.Data { + ids[i], _ = ret.Data[i].GetString("id") + } + return ids, nil +} + +func GetUsernameByID(id string) (string, error) { + user, err := GetUserByID(id) + if err != nil { + return "", err + } + name, _ := user.GetString("name") + return name, nil +} diff --git a/pkg/notify/utils/others.go b/pkg/notify/utils/others.go new file mode 100644 index 0000000000..12098d7177 --- /dev/null +++ b/pkg/notify/utils/others.go @@ -0,0 +1,60 @@ +// Copyright 2019 Yunion +// +// 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 utils + +import ( + "bytes" + "context" + "fmt" + "math/rand" + "time" + + "yunion.io/x/jsonutils" + + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/util/rbacutils" +) + +func FetchOwnerId(ctx context.Context, manager db.IModelManager, userCred mcclient.TokenCredential, data jsonutils.JSONObject) (mcclient.IIdentityProvider, error) { + var ownerId mcclient.IIdentityProvider + var err error + if manager.ResourceScope() != rbacutils.ScopeSystem { + ownerId, err = manager.FetchOwnerId(ctx, data) + if err != nil { + return nil, httperrors.NewGeneralError(err) + } + } + if ownerId == nil { + ownerId = userCred + } + return ownerId, nil +} + +func GenerateMobileToken() string { + rnd := rand.New(rand.NewSource(time.Now().UnixNano())) + token := fmt.Sprintf("%06v", rnd.Int31n(1000000)) + return token +} + +func GenerateEmailToken(tokenLen int) string { + rnd := rand.New(rand.NewSource(time.Now().UnixNano())) + token := new(bytes.Buffer) + for token.Len() < tokenLen { + token.WriteString(fmt.Sprintf("%x", rnd.Int31())) + } + return token.String()[:tokenLen] +} diff --git a/pkg/notify/utils/utils_test.go b/pkg/notify/utils/utils_test.go new file mode 100644 index 0000000000..08eff8c86c --- /dev/null +++ b/pkg/notify/utils/utils_test.go @@ -0,0 +1,33 @@ +// Copyright 2019 Yunion +// +// 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 utils + +import ( + "testing" +) + +func TestGenEmailToken(t *testing.T) { + s := GenerateEmailToken(32) + if len(s) != 32 { + t.Error("email token length should be 32") + } +} + +func TestGenMobileToke(t *testing.T) { + s := GenerateMobileToken() + if len(s) != 6 { + t.Errorf("mobile token length should be 6") + } +}