diff --git a/build/notify/root/opt/yunion/share/notify/email_verify_template b/build/notify/root/opt/yunion/share/notify/email_verify_template new file mode 100644 index 0000000000..0f4909da9e --- /dev/null +++ b/build/notify/root/opt/yunion/share/notify/email_verify_template @@ -0,0 +1,66 @@ + + + + + Document + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+
+ + + + + + + + +
+ 产品 + + 解决方案 + + 服务 + + 帮助与文档 + + 关于我们 +
+
+ + + + + + + + + + +
+ 亲爱的{{.name}},您好: +
请点击下面的链接,完成用户邮箱的激活:
{{.link}}
+
*为了确保您的帐号安全,该链接仅48小时内访问有效,请勿直接回复此邮件。
版权所有 © 北京云联万维科技有限公司 2018 保留一切权利 京ICP备17065753号
+ + diff --git a/cmd/climc/shell/contacts.go b/cmd/climc/shell/contacts.go index 8f37c44645..57c8817b78 100644 --- a/cmd/climc/shell/contacts.go +++ b/cmd/climc/shell/contacts.go @@ -167,17 +167,10 @@ func init() { } R(&ContactsVerifyOptions{}, "contact-verify-trigger", "Trigger contact verify", func(s *mcclient.ClientSession, args *ContactsVerifyOptions) error { params := jsonutils.NewDict() - params.Add(jsonutils.NewString(args.CONTACT_TYPE), "contact_type") - params.Add(jsonutils.NewString(args.CONTACT), "contact") - /* - if len(args.Email) > 0 { - params.Add(jsonutils.NewString(args.Email), "email") - } - if len(args.Mobile) > 0 { - params.Add(jsonutils.NewString(args.Mobile), "mobile") - } - */ - + tmpDict := jsonutils.NewDict() + tmpDict.Add(jsonutils.NewString(args.CONTACT_TYPE), "contact_type") + tmpDict.Add(jsonutils.NewString(args.CONTACT), "contact") + params.Add(tmpDict, "contact") _, err := modules.Contacts.PerformAction(s, args.UID, "verify", params) if err != nil { return err diff --git a/cmd/climc/shell/notification.go b/cmd/climc/shell/notification.go index a3d0320809..adffdb4d94 100644 --- a/cmd/climc/shell/notification.go +++ b/cmd/climc/shell/notification.go @@ -37,7 +37,9 @@ func init() { Remark string `help:"Remark or description of the notification"` Group bool `help:"Send to group"` } - R(&NotificationCreateOptions{}, "notify", "Send a notification to sb", func(s *mcclient.ClientSession, args *NotificationCreateOptions) error { + R(&NotificationCreateOptions{}, "notify", "Send a notification to someones", func(s *mcclient.ClientSession, + args *NotificationCreateOptions) error { + msg := notify.SNotifyMessage{} if args.Group { msg.Gid = args.Uid diff --git a/cmd/climc/shell/notify_template.go b/cmd/climc/shell/notify_template.go new file mode 100644 index 0000000000..b635e8eac6 --- /dev/null +++ b/cmd/climc/shell/notify_template.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 shell + +import ( + "yunion.io/x/jsonutils" + + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/mcclient/modules" + "yunion.io/x/onecloud/pkg/mcclient/options" +) + +func init() { + type NotifyTemplateUpdateOptions struct { + CONTACTTYPE string `help:"the contanct type, such as 'email', 'mobile'"` + Topic string `help:"the topic of temlate, such as 'VERIFY', 'ALARM'"` + TemplateType string `help:"the type of template" choices:"content|title|remote"` + Content string `help:"the content of template"` + } + R(&NotifyTemplateUpdateOptions{}, "notify-template-update", "Create, update contact for user", func(s *mcclient.ClientSession, + args *NotifyTemplateUpdateOptions) error { + arr := jsonutils.NewArray() + tmpObj := jsonutils.NewDict() + tmpObj.Add(jsonutils.NewString(args.Topic), "topic") + tmpObj.Add(jsonutils.NewString(args.TemplateType), "template_type") + tmpObj.Add(jsonutils.NewString(args.Content), "content") + arr.Add(tmpObj) + + params := jsonutils.NewDict() + params.Add(arr, "notifytemplates") + + contact, err := modules.NotifyTemplates.PerformAction(s, args.CONTACTTYPE, "update-template", params) + + if err != nil { + return err + } + + printObject(contact) + return nil + }) + + type NotifyTemplateDeleteOptions struct { + CONTACTTYPE string `help:"the contanct type, such as 'email', 'mobile'"` + Topic string `help:"the topic of temlate, such as 'VERIFY', 'ALARM'"` + } + + R(&NotifyTemplateDeleteOptions{}, "notify-template-delete", "delete notify template", + func(s *mcclient.ClientSession, args *NotifyTemplateDeleteOptions) error { + + tmpObj := jsonutils.NewDict() + tmpObj.Add(jsonutils.NewString(args.CONTACTTYPE), "contact_type") + tmpObj.Add(jsonutils.NewString(args.Topic), "topic") + _, err := modules.NotifyTemplates.DeleteContents(s, tmpObj) + if err != nil { + return err + } + return nil + }) + + type NotifyTemplateListOptions struct { + options.BaseListOptions + } + + R(&NotifyTemplateListOptions{}, "notify-template-list", "List all notify template", + func(s *mcclient.ClientSession, args *NotifyTemplateListOptions) error { + + params, err := args.BaseListOptions.Params() + if err != nil { + return err + } + result, err := modules.NotifyTemplates.List(s, params) + if err != nil { + return err + } + printList(result, modules.NotifyTemplates.GetColumns(s)) + return nil + }) +} diff --git a/pkg/cloudcommon/db/usercache.go b/pkg/cloudcommon/db/usercache.go index 6a89f46b91..2a76b4587a 100644 --- a/pkg/cloudcommon/db/usercache.go +++ b/pkg/cloudcommon/db/usercache.go @@ -103,10 +103,10 @@ func (manager *SUserCacheManager) fetchUser( return user, nil } } - return manager.fetchUserFromKeystone(ctx, idStr) + return manager.FetchUserFromKeystone(ctx, idStr) } -func (manager *SUserCacheManager) fetchUserFromKeystone(ctx context.Context, idStr string) (*SUser, error) { +func (manager *SUserCacheManager) FetchUserFromKeystone(ctx context.Context, idStr string) (*SUser, error) { if len(idStr) == 0 { log.Debugf("fetch empty user!!!!\n%s", debug.Stack()) return nil, fmt.Errorf("Empty idStr") diff --git a/pkg/mcclient/modules/mod_contacts.go b/pkg/mcclient/modules/mod_contacts.go index 6f8480d440..609f8dd94a 100644 --- a/pkg/mcclient/modules/mod_contacts.go +++ b/pkg/mcclient/modules/mod_contacts.go @@ -16,6 +16,7 @@ package modules import ( "fmt" + "net/url" "yunion.io/x/jsonutils" @@ -44,6 +45,21 @@ func (this *ContactsManager) DoBatchDeleteContacts(s *mcclient.ClientSession, pa return modulebase.Post(this.ResourceManager, s, path, params, this.Keyword) } +func (this *ContactsManager) PerformAction(session *mcclient.ClientSession, id string, action string, + params jsonutils.JSONObject) (jsonutils.JSONObject, error) { + + path := fmt.Sprintf("/%s/%s/%s?uname=true", this.ContextPath(nil), url.PathEscape(id), url.PathEscape(action)) + return modulebase.Post(this.ResourceManager, session, path, params, this.KeywordPlural) +} + +func (this *ContactsManager) Get(session *mcclient.ClientSession, id string, params jsonutils.JSONObject) (jsonutils.JSONObject, + error) { + + q := params.(*jsonutils.JSONDict) + q.Add(jsonutils.JSONTrue, "uname") + return this.ResourceManager.Get(session, id, params) +} + var ( Contacts ContactsManager ) diff --git a/pkg/mcclient/modules/mod_notify_template.go b/pkg/mcclient/modules/mod_notify_template.go new file mode 100644 index 0000000000..7017473305 --- /dev/null +++ b/pkg/mcclient/modules/mod_notify_template.go @@ -0,0 +1,43 @@ +// 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 modules + +import ( + "yunion.io/x/jsonutils" + + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/mcclient/modulebase" +) + +type NotifyTemplateManager struct { + modulebase.ResourceManager +} + +func (nm *NotifyTemplateManager) DeleteContents(s *mcclient.ClientSession, params jsonutils.JSONObject) (jsonutils.JSONObject, error) { + path := "/templates/delete-template" + return modulebase.Post(nm.ResourceManager, s, path, params, "") +} + +var ( + NotifyTemplates NotifyTemplateManager +) + +func init() { + NotifyTemplates = NotifyTemplateManager{NewNotifyManager("notifytemplate", "notifytemplates", + []string{"Contact_Type", "Topic", "Template_Type"}, + []string{})} + + register(&NotifyTemplates) +} diff --git a/pkg/mcclient/modules/notify/mod_notification.go b/pkg/mcclient/modules/notify/mod_notification.go index 330ba34b3c..21ffb8f5f5 100644 --- a/pkg/mcclient/modules/notify/mod_notification.go +++ b/pkg/mcclient/modules/notify/mod_notification.go @@ -42,7 +42,8 @@ type NotificationManager struct { } func (manager *NotificationManager) Send(s *mcclient.ClientSession, msg SNotifyMessage) error { - _, err := manager.Create(s, jsonutils.Marshal(&msg)) + path := "/" + manager.KeywordPlural + _, err := modulebase.Post(manager.ResourceManager, s, path, jsonutils.Marshal(&msg), manager.KeywordPlural) return err } diff --git a/pkg/notify/cache/usercache.go b/pkg/notify/cache/usercache.go index 90303ca9aa..2ee25e96e4 100644 --- a/pkg/notify/cache/usercache.go +++ b/pkg/notify/cache/usercache.go @@ -18,26 +18,21 @@ import ( "context" "database/sql" "fmt" - "time" - "yunion.io/x/log" "yunion.io/x/pkg/errors" - "yunion.io/x/onecloud/pkg/cloudcommon/consts" "yunion.io/x/onecloud/pkg/cloudcommon/db" - "yunion.io/x/onecloud/pkg/cloudcommon/db/lockman" "yunion.io/x/onecloud/pkg/mcclient" "yunion.io/x/onecloud/pkg/mcclient/auth" - "yunion.io/x/onecloud/pkg/mcclient/modules" "yunion.io/x/onecloud/pkg/util/httputils" ) type SUserCacheManager struct { - db.SKeystoneCacheObjectManager + db.SUserCacheManager } type SUser struct { - db.SKeystoneCacheObject + db.SUser } func (user *SUser) GetModelManager() db.IModelManager { @@ -46,13 +41,6 @@ func (user *SUser) GetModelManager() db.IModelManager { var UserCacheManager *SUserCacheManager -func init() { - UserCacheManager = &SUserCacheManager{ - db.NewKeystoneCacheObjectManager(SUser{}, "users_cache_tbl", "user", "users")} - // log.Debugf("initialize user cache manager %s", UserCacheManager.KeywordPlural()) - UserCacheManager.SetVirtualObject(UserCacheManager) -} - func RegistUserCredCacheUpdater() { auth.RegisterAuthHook(onAuthCompleteUpdateCache) } @@ -63,97 +51,18 @@ func onAuthCompleteUpdateCache(userCred mcclient.TokenCredential) { func (ucm *SUserCacheManager) updateUserCache(userCred mcclient.TokenCredential) { ucm.Save(context.Background(), userCred.GetUserId(), userCred.GetUserName(), - userCred.GetDomainId()) + userCred.GetDomainId(), userCred.GetDomainName()) } -func (ucm *SUserCacheManager) FetchUserByIdOrName(idStr string) (*SUser, error) { - obj, err := ucm.SKeystoneCacheObjectManager.FetchByIdOrName(nil, idStr) - if err != nil { - return nil, err - } - return obj.(*SUser), nil -} - -func (ucm *SUserCacheManager) FetchUserById(idStr string) (*SUser, error) { - obj, err := ucm.SKeystoneCacheObjectManager.FetchById(idStr) - if err != nil { - return nil, err - } - return obj.(*SUser), nil -} - -func (ucm *SUserCacheManager) FetchUserByName(idStr string) (*SUser, error) { - obj, err := ucm.SKeystoneCacheObjectManager.FetchByName(nil, idStr) - if err != nil { - return nil, err - } - return obj.(*SUser), nil -} - -func (ucm *SUserCacheManager) Save(ctx context.Context, idStr string, name string, domainId string) (*SUser, error) { - lockman.LockRawObject(ctx, ucm.KeywordPlural(), idStr) - defer lockman.ReleaseRawObject(ctx, ucm.KeywordPlural(), idStr) - - objo, err := ucm.FetchById(idStr) - if err != nil && err != sql.ErrNoRows { - log.Errorf("FetchTenantbyId fail %s", err) - return nil, err - } - now := time.Now().UTC() - if err == nil { - obj := objo.(*SUser) - if obj.Id == idStr && obj.Name == name && obj.DomainId == domainId { - db.Update(obj, func() error { - obj.LastCheck = now - return nil - }) - return obj, nil - } - _, err = db.Update(obj, func() error { - obj.Id = idStr - obj.Name = name - obj.DomainId = domainId - obj.LastCheck = now - return nil - }) - if err != nil { - return nil, err - } else { - return obj, nil - } - } else { - objm, err := db.NewModelObject(ucm) - obj := objm.(*SUser) - obj.Id = idStr - obj.Name = name - obj.DomainId = domainId - obj.LastCheck = now - err = ucm.TableSpec().Insert(obj) - if err != nil { - return nil, err - } else { - return obj, nil - } - } -} - -func (ucm *SUserCacheManager) fetchUserFromKeystone(ctx context.Context, idStr string) (*SUser, error) { - if len(idStr) == 0 { - return nil, fmt.Errorf("Empty idStr") - } - s := auth.GetAdminSession(ctx, consts.GetRegion(), "v3") - user, err := modules.UsersV3.GetById(s, idStr, nil) +func (ucm *SUserCacheManager) dealErrFromKeystone(err error) error { if err != nil { if je, ok := err.(*httputils.JSONClientError); ok && je.Code == 404 { - return nil, sql.ErrNoRows + return sql.ErrNoRows + } else { + return errors.Wrap(err, "fetch User info from keystone") } - log.Errorf("fetch project %s fail %s", idStr, err) - return nil, errors.Wrap(err, "modules.Projects.Get") } - userId, _ := user.GetString("id") - userName, _ := user.GetString("name") - domainId, _ := user.GetString("domain_id") - return ucm.Save(ctx, userId, userName, domainId) + return nil } func (ucm *SUserCacheManager) FetchUsersByIDs(ctx context.Context, ids []string) (map[string]SUser, error) { @@ -174,32 +83,21 @@ func (ucm *SUserCacheManager) FetchUsersByIDs(ctx context.Context, ids []string) if _, ok := ret[id]; ok { continue } - user, err := ucm.fetchUserFromKeystone(ctx, id) + user, err := ucm.FetchUserFromKeystone(ctx, id) if err != nil { continue } - ret[id] = *user + ret[id] = SUser{*user} } return ret, nil } -func (ucm *SUserCacheManager) FetchUserByID(ctx context.Context, idStr string, noExpireCheck bool) (*SUser, error) { - - q := ucm.Query().Equals("id", idStr) - uobj, err := db.NewModelObject(ucm) +func (ucm *SUserCacheManager) FetchUserByIDOrName(ctx context.Context, idStr string) (*SUser, error) { + user, err := ucm.SUserCacheManager.FetchUserByIdOrName(ctx, idStr) if err != nil { - return nil, errors.Wrap(err, "NewModelObject") + return nil, err } - err = q.First(uobj) - if err != nil && err != sql.ErrNoRows { - return nil, errors.Wrap(err, "query") - } else if uobj != nil { - user := uobj.(*SUser) - if noExpireCheck || !user.IsExpired() { - return user, nil - } - } - return ucm.fetchUserFromKeystone(ctx, idStr) + return &SUser{*user}, nil } func (ucm *SUserCacheManager) FetchUserLikeName(ctx context.Context, name string, noExpireCheck bool) ([]SUser, diff --git a/pkg/notify/dispatcher.go b/pkg/notify/dispatcher.go index e3e6139528..c45d9a8a8b 100644 --- a/pkg/notify/dispatcher.go +++ b/pkg/notify/dispatcher.go @@ -24,6 +24,7 @@ import ( "yunion.io/x/jsonutils" "yunion.io/x/log" "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/util/sets" "yunion.io/x/pkg/utils" "yunion.io/x/sqlchemy" @@ -228,8 +229,8 @@ func (self *NotifyModelDispatcher) VerifyTrigger(ctx context.Context, params map 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)) + if err != nil || len(contacts) == 0 { + return nil, errors.Error(fmt.Sprintf("uid '%s' don't have contact '%s' of contact_type '%s'", uid, contact, contactType)) } userCred := policy.FetchUserCredential(ctx) scontact := contacts[0] @@ -262,12 +263,16 @@ func (self *NotifyModelDispatcher) VerifyTrigger(ctx context.Context, params map } if scontact.Status == models.CONTACT_VERIFYING { verifications, err := models.VerifyManager.FetchByCID(scontact.ID, func(q *sqlchemy.SQuery) *sqlchemy.SQuery { - q = q.Equals("status", models.VERIFICATION_SENT).Desc("created_at") + q = q.In("status", []string{models.VERIFICATION_SENT, "init"}).Desc("created_at") return q }) if err != nil { return nil, httperrors.NewGeneralError(err) } + if len(verifications) == 0 { + // no verifications in status "sent" + return makeNewVerify() + } current := time.Now() for _, verification := range verifications { if current.After(verification.ExpireAt) { @@ -292,8 +297,11 @@ func (self *NotifyModelDispatcher) DeleteContacts(ctx context.Context, uids2 []j for i := range uids2 { uids[i] = strings.Trim(uids2[i].String(), `"`) } - - contacts, err := models.ContactManager.FetchByUIDs(uids) + uname := false + if v := ctx.Value("uname"); v != nil { + uname = true + } + contacts, err := models.ContactManager.FetchByUIDs(uids, uname) if err != nil { return httperrors.NewGeneralError(err) } @@ -425,6 +433,91 @@ func (self *NotifyModelDispatcher) UpdateContacts(ctx context.Context, idstr str return nil } +func (self *NotifyModelDispatcher) UpdateTemplate(ctx context.Context, ctype string, query jsonutils.JSONObject, + datas []jsonutils.JSONObject) error { + + type sTemplate struct { + ContactType string + Topic string + TemplateType string + Content string + } + templates := make([]sTemplate, 0, len(datas)) + topics := sets.NewString() + for _, data := range datas { + var tem sTemplate + err := data.Unmarshal(&tem) + if err != nil { + return errors.Wrap(err, "data.Unmarshal") + } + if tem.TemplateType != models.TEMPLATE_TYPE_REMOTE && tem.TemplateType != models. + TEMPLATE_TYPE_CONTENT && tem.TemplateType != models.TEMPLATE_TYPE_TITLE { + + return httperrors.NewInputParameterError("no support for such template type '%s'", tem.TemplateType) + } + tem.ContactType = ctype + tem.Topic = strings.ToUpper(tem.Topic) + templates = append(templates, tem) + topics.Insert(tem.Topic) + } + + q := models.TemplateManager.Query().Equals("contact_type", ctype).In("topic", topics.List()) + templateModels := make([]models.STemplate, 0, 1) + err := db.FetchModelObjects(models.ContactManager, q, &templateModels) + if err != nil { + log.Errorf("db.FetchModelObjects sql: %s", q.String()) + return errors.Wrap(err, "db.FetchModelObjects") + } + + templateMaps := make(map[string]*models.STemplate) + for i := range templateModels { + k := fmt.Sprintf("%s/%s/%s", templateModels[i].ContactType, templateModels[i].Topic, templateModels[i].TemplateType) + templateMaps[k] = &templateModels[i] + } + + userCred := policy.FetchUserCredential(ctx) + for _, tem := range templates { + k := fmt.Sprintf("%s/%s/%s", tem.ContactType, tem.Topic, tem.TemplateType) + if tmod, ok := templateMaps[k]; ok { + updateData := jsonutils.NewDict() + updateData.Add(jsonutils.NewString(tem.Content), "content") + err = UpdateItem(models.TemplateManager, tmod, ctx, userCred, query, updateData) + if err != nil { + return errors.Wrapf(err, "fail to update template '%s'", tmod.ID) + } + continue + } + _, err = self.Create(ctx, query, jsonutils.Marshal(tem), nil) + if err != nil { + return errors.Wrapf(err, "fail to create template(contact_type: %s, topic: %s, template_type: %s)", + tem.ContactType, tem.Topic, tem.TemplateType) + } + } + return nil +} + +func (self *NotifyModelDispatcher) DeleteTemplate(ctx context.Context, query jsonutils.JSONObject, ctype, topic string) error { + + q := models.TemplateManager.Query().Equals("contact_type", ctype) + if len(topic) != 0 { + q = q.Equals("topic", topic) + } + templates := make([]models.STemplate, 0, 1) + err := db.FetchModelObjects(models.TemplateManager, q, &templates) + if err != nil { + log.Errorf("db.FetchModelObjects sql: %s", q.String()) + return errors.Wrap(err, "db.FetchModelObjects") + } + userCred := policy.FetchUserCredential(ctx) + for i := range templates { + err = DeleteItem(&templates[i], ctx, userCred, query, jsonutils.JSONNull) + if err != nil { + return errors.Wrapf(err, "fail to delete template '%s'", templates[i].ID) + } + } + return 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) diff --git a/pkg/notify/handlers.go b/pkg/notify/handlers.go index a7b58c0dc0..b0d0990029 100644 --- a/pkg/notify/handlers.go +++ b/pkg/notify/handlers.go @@ -18,12 +18,15 @@ import ( "context" "fmt" "net/http" + "net/url" "yunion.io/x/jsonutils" "yunion.io/x/log" "yunion.io/x/pkg/errors" + "yunion.io/x/onecloud/pkg/appctx" "yunion.io/x/onecloud/pkg/appsrv" + "yunion.io/x/onecloud/pkg/cloudcommon/consts" "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/cloudcommon/policy" "yunion.io/x/onecloud/pkg/httperrors" @@ -44,9 +47,35 @@ func InitHandlers(app *appsrv.Application) { db.RegisterModelManager(models.ConfigManager) db.RegisterModelManager(cache.UserCacheManager) db.RegisterModelManager(cache.UserGroupCacheManager) + db.RegisterModelManager(models.TemplateManager) AddNotifyDispatcher("/api/v1/", app) } +// Middleware +func middleware(f appsrv.FilterHandler) appsrv.FilterHandler { + hander := func(ctx context.Context, w http.ResponseWriter, r *http.Request) { + if _, ok := r.URL.Query()["uname"]; ok { + // Uname + params := appctx.AppContextParams(ctx) + if uid, ok := params[""]; ok { + userDetail, err := utils.GetUserByIDOrName(ctx, uid) + if err != nil { + httperrors.NotFoundError(w, "Uid or Uname Not Found") + return + } + params[""] = userDetail.Id + } + ctx = context.WithValue(ctx, "uname", true) + } + f(ctx, w, r) + } + if consts.IsRbacEnabled() { + return auth.AuthenticateWithDelayDecision(hander, true) + } else { + return auth.Authenticate(hander) + } +} + func AddNotifyDispatcher(prefix string, app *appsrv.Application) { var metadata map[string]interface{} var tags map[string]string @@ -56,89 +85,167 @@ func AddNotifyDispatcher(prefix string, app *appsrv.Application) { metadata, tags = map[string]interface{}{"manager": modelDispatcher}, map[string]string{"resource": modelDispatcher.KeywordPlural()} app.AddHandler2("POST", fmt.Sprintf("%s/%s//update-contact", prefix, modelDispatcher.KeywordPlural()), - modelDispatcher.Filter(contactUpdateHandler), metadata, "contact_update", tags) + middleware(contactUpdateHandler), metadata, "contact_update", tags) // List app.AddHandler2("GET", fmt.Sprintf("%s/%s", prefix, modelDispatcher.KeywordPlural()), - modelDispatcher.Filter(listHandler), metadata, "list_contacts", tags) + middleware(listHandler), metadata, "list_contacts", tags) app.AddHandler2("GET", fmt.Sprintf("%s/%s/users", prefix, modelDispatcher.KeywordPlural()), - modelDispatcher.Filter(keyStoneUserListHandler), metadata, "list_users", tags) + middleware(keyStoneUserListHandler), metadata, "list_users", tags) app.AddHandler2("GET", fmt.Sprintf("%s/%s/", prefix, modelDispatcher.KeywordPlural()), - modelDispatcher.Filter(getHandler), metadata, "list_by_uid", tags) + middleware(getHandler), metadata, "list_by_uid", tags) app.AddHandler2("POST", fmt.Sprintf("%s/%s/delete-contact", prefix, modelDispatcher.KeywordPlural()), - modelDispatcher.Filter(deleteContactHandler), metadata, "delete", tags) + middleware(deleteContactHandler), metadata, "delete", tags) // verify-trigger app.AddHandler2("POST", fmt.Sprintf("%s/%s//verify", prefix, modelDispatcher.KeywordPlural()), - modelDispatcher.Filter(verifyTriggerHandler), metadata, "verify_trigger", tags) + middleware(verifyTriggerHandler), metadata, "verify_trigger", tags) // 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()} app.AddHandler2("GET", fmt.Sprintf("%s/%s/", prefix, models.VerifyManager.KeywordPlural()), - modelDispatcher.Filter(verifyHandler), metadata, "verify", tags) + middleware(verifyHandler), metadata, "verify", tags) // notification Handler modelDispatcher = NewNotifyModelDispatcher(models.NotificationManager) metadata, tags = map[string]interface{}{"manager": modelDispatcher}, map[string]string{"resource": modelDispatcher.KeywordPlural()} app.AddHandler2("POST", fmt.Sprintf("%s/%s/", prefix, modelDispatcher.KeywordPlural()), - modelDispatcher.Filter(notificationHandler), metadata, "send_notifications", tags) + middleware(notificationHandler), metadata, "send_notifications", tags) app.AddHandler2("GET", fmt.Sprintf("%s/%s/", prefix, modelDispatcher.KeywordPlural()), - modelDispatcher.Filter(listHandler), metadata, "send_notifications", tags) + middleware(listHandler), metadata, "send_notifications", tags) app.AddHandler2("GET", fmt.Sprintf("%s/%s/", prefix, modelDispatcher.KeywordPlural()), - modelDispatcher.Filter(listHandler), metadata, "list_notification_by_id", tags) + middleware(listHandler), metadata, "list_notification_by_id", tags) // config Handler modelDispatcher = NewNotifyModelDispatcher(models.ConfigManager) metadata, tags = map[string]interface{}{"manager": modelDispatcher}, map[string]string{"resource": modelDispatcher.KeywordPlural()} app.AddHandler2("POST", fmt.Sprintf("%s/%s/", prefix, modelDispatcher.KeywordPlural()), - modelDispatcher.Filter(configUpdateHandler), metadata, "update_configs", tags) + middleware(configUpdateHandler), metadata, "update_configs", tags) app.AddHandler2("GET", fmt.Sprintf("%s/%s/", prefix, modelDispatcher.KeywordPlural()), - modelDispatcher.Filter(configGetHandler), metadata, "get_configs", tags) + middleware(configGetHandler), metadata, "get_configs", tags) app.AddHandler2("DELETE", fmt.Sprintf("%s/%s/", prefix, modelDispatcher.KeywordPlural()), - modelDispatcher.Filter(configDeleteHandler), metadata, "delete_configs", tags) + middleware(configDeleteHandler), metadata, "delete_configs", tags) // email handler for being compatible app.AddHandler2("POST", fmt.Sprintf("%s/%s/", prefix, EMAIL_KEYWORDPLURAL), - modelDispatcher.Filter(emailConfigUpdateHandler), metadata, "", tags) + middleware(emailConfigUpdateHandler), metadata, "", tags) app.AddHandler2("GET", fmt.Sprintf("%s/%s/", prefix, EMAIL_KEYWORDPLURAL), - modelDispatcher.Filter(emailConfigGetHandler), metadata, "", tags) + middleware(emailConfigGetHandler), metadata, "", tags) app.AddHandler2("DELETE", fmt.Sprintf("%s/%s/", prefix, EMAIL_KEYWORDPLURAL), - modelDispatcher.Filter(emailConfigDeleteHandler), metadata, "", tags) + middleware(emailConfigDeleteHandler), metadata, "", tags) app.AddHandler2("PUT", fmt.Sprintf("%s/%s/", prefix, EMAIL_KEYWORDPLURAL), - modelDispatcher.Filter(emailConfigUpdateHandler), metadata, "", tags) + middleware(emailConfigUpdateHandler), metadata, "", tags) app.AddHandler2("POST", fmt.Sprintf("%s/%s/", prefix, SMS_KEYWORDPLURAL), - modelDispatcher.Filter(smsConfigUpdateHandler), metadata, "", tags) + middleware(smsConfigUpdateHandler), metadata, "", tags) app.AddHandler2("GET", fmt.Sprintf("%s/%s/", prefix, SMS_KEYWORDPLURAL), - modelDispatcher.Filter(smsConfigGetHandler), metadata, "", tags) + middleware(smsConfigGetHandler), metadata, "", tags) app.AddHandler2("DELETE", fmt.Sprintf("%s/%s/", prefix, SMS_KEYWORDPLURAL), - modelDispatcher.Filter(smsConfigDeleteHandler), metadata, "", tags) + middleware(smsConfigDeleteHandler), metadata, "", tags) app.AddHandler2("PUT", fmt.Sprintf("%s/%s/", prefix, SMS_KEYWORDPLURAL), - modelDispatcher.Filter(smsConfigUpdateHandler), metadata, "", tags) + middleware(smsConfigUpdateHandler), metadata, "", tags) + // Contact Handler + modelDispatcher = NewNotifyModelDispatcher(models.TemplateManager) + metadata, tags = map[string]interface{}{"manager": modelDispatcher}, map[string]string{"resource": modelDispatcher.KeywordPlural()} + app.AddHandler2("POST", + fmt.Sprintf("%s/%s//update-template", prefix, modelDispatcher.KeywordPlural()), + middleware(templateUpdateHandler), metadata, "update_template", tags) + // List + app.AddHandler2("GET", + fmt.Sprintf("%s/%s", prefix, modelDispatcher.KeywordPlural()), + middleware(listHandler), metadata, "list_template", tags) + + app.AddHandler2("POST", + fmt.Sprintf("%s/%s/delete-template", prefix, modelDispatcher.KeywordPlural()), + middleware(deleteTemplateHandler), metadata, "delete", tags) + + app.AddHandler2("POST", + fmt.Sprintf("%s/%s/email-url", prefix, modelDispatcher.KeywordPlural()), + middleware(updateEmailUrlHandler), metadata, "update_email_url", tags) + + app.AddHandler2("GET", + fmt.Sprintf("%s/%s/email-url", prefix, modelDispatcher.KeywordPlural()), + middleware(getEmailUrlHandler), metadata, "get_email_url", tags) +} + +func updateEmailUrlHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + userCred := policy.FetchUserCredential(ctx) + if !userCred.HasSystemAdminPrivilege() { + httperrors.ForbiddenError(w, "only system admin can update email url") + return + } + _, _, _, body := fetchEnv(ctx, w, r) + if !body.Contains("email_url") { + httperrors.InputParameterError(w, "miss email_url") + } + emailUrl, _ := body.GetString("email_url") + eUrl, err := url.Parse(emailUrl) + if err != nil { + httperrors.InputParameterError(w, "invalid url") + } + models.TemplateManager.SetEmailUrl(eUrl.String()) +} + +func getEmailUrlHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + ret := jsonutils.NewDict() + ret.Add(jsonutils.NewString(models.TemplateManager.GetEmailUrl()), "email_url") + appsrv.Send(w, ret.PrettyString()) +} + +func templateUpdateHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + manager, params, query, body := fetchEnv(ctx, w, r) + data, err := body.GetArray(manager.Keyword(), manager.KeywordPlural()) + if err != nil { + httperrors.GeneralServerError(w, httperrors.NewInputParameterError("need %s or %s", manager.Keyword(), + manager.KeywordPlural())) + return + } + ctype := params[""] + if len(ctype) == 0 { + httperrors.InputParameterError(w, "ctype of template should not be empty") + } + err = manager.UpdateTemplate(ctx, ctype, mergeQueryParams(params, query), data) + if err != nil { + httperrors.GeneralServerError(w, err) + } +} + +func deleteTemplateHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { + manager, _, query, body := fetchEnv(ctx, w, r) + if !body.Contains("contact_type") { + httperrors.InputParameterError(w, "miss contact_type") + return + } + ctype, _ := body.GetString("contact_type") + topic, _ := body.GetString("topic") + err := manager.DeleteTemplate(ctx, query, ctype, topic) + if err != nil { + httperrors.GeneralServerError(w, err) + } } func configDeleteHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) { @@ -211,14 +318,8 @@ func contactUpdateHandler(ctx context.Context, w http.ResponseWriter, r *http.Re manager.KeywordPlural())) return } - // check that if the uid is exist + uid := params[""] - _, err = utils.GetUserByID(ctx, uid) - if err != nil { - log.Errorf(`uid %q not found`, uid) - httperrors.NotFoundError(w, "Uid Not Found") - return - } queryDict := mergeQueryParams(params, query) update, _ := body.Bool(manager.Keyword(), "update_dingtalk") if update { @@ -258,7 +359,7 @@ func deleteContactHandler(ctx context.Context, w http.ResponseWriter, r *http.Re // 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()) + data, err := body.Get(models.ContactManager.Keyword(), models.ContactManager.KeywordPlural()) if err != nil { httperrors.BadRequestError(w, "request body should have %s", manager.KeywordPlural()) return diff --git a/pkg/notify/interface/interface.go b/pkg/notify/interface/interface.go index 97027b669a..44eaaccac5 100644 --- a/pkg/notify/interface/interface.go +++ b/pkg/notify/interface/interface.go @@ -18,6 +18,7 @@ import ( "context" "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/notify/rpc/apis" ) type INotifyService interface { @@ -34,4 +35,8 @@ type IServiceConfigStore interface { SetConfig(serviceName string, config SConfig) error } +type ITemplateStore interface { + NotifyFilter(contactType, topic, msg string) (params apis.SendParams, err error) +} + type SConfig map[string]string diff --git a/pkg/notify/models/initdb.go b/pkg/notify/models/initdb.go index e662fca861..f82e4a4760 100644 --- a/pkg/notify/models/initdb.go +++ b/pkg/notify/models/initdb.go @@ -31,6 +31,7 @@ func InitDB() error { VerifyManager, NotificationManager, ConfigManager, + TemplateManager, } { err := manager.InitializeData() if err != nil { diff --git a/pkg/notify/models/mod_contact.go b/pkg/notify/models/mod_contact.go index 2667a4b053..c102390219 100644 --- a/pkg/notify/models/mod_contact.go +++ b/pkg/notify/models/mod_contact.go @@ -22,6 +22,7 @@ import ( "yunion.io/x/jsonutils" "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/util/sets" "yunion.io/x/sqlchemy" "yunion.io/x/onecloud/pkg/cloudcommon/db" @@ -99,20 +100,57 @@ func (self *SContactManager) InitializeData() error { return nil } -func (self *SContactManager) FetchByUIDs(uids []string) ([]SContact, error) { +func (self *SContactManager) FetchByUIDs(uids []string, uname bool) ([]SContact, error) { + var err error + if uname { + uids, err = self._UIDsFromUIDOrName(uids) + if err != nil { + return nil, err + } + } q := self.Query() q = q.Filter(sqlchemy.In(q.Field("uid"), uids)) records := make([]SContact, 0, len(uids)) - err := db.FetchModelObjects(self, q, &records) + err = db.FetchModelObjects(self, q, &records) if err != nil { return nil, err } return records, nil } +func (self *SContactManager) _UIDsFromUIDOrName(uidStrs []string) ([]string, error) { + users, err := utils.GetUsersWithoutRemote(uidStrs) + if err != nil { + return nil, err + } + uids := make([]string, 0, len(uidStrs)) + uidSet := sets.NewString(uidStrs...) + var ( + uid string + uname string + ) + for i := range users { + uid = users[i].Id + uname = users[i].Name + if uidSet.Has(uid) { + uids = append(uids, uid) + uidSet.Delete(uid) + continue + } + if uidSet.Has(uname) { + uids = append(uids, uid) + uidSet.Delete(uname) + continue + } + } + for _, uid = range uidSet.UnsortedList() { + uids = append(uids, uid) + } + return uids, 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))) + q := self.Query("id", "uid", "contact_type", "contact", "enabled").Equals("uid", uid).In("contact_type", contactTypes) records := make([]SContact, 0, len(contactTypes)) err := db.FetchModelObjects(self, q, &records) if err != nil { @@ -122,8 +160,7 @@ func (self *SContactManager) FetchByUIDAndCType(uid string, contactTypes []strin } 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))) + q := self.Query().Equals("uid", uid).Equals("contact", contact).Equals("contact_type", contactType) records := make([]SContact, 0, 1) err := db.FetchModelObjects(self, q, &records) if err != nil { @@ -215,6 +252,12 @@ func (self *SContactManager) GetAllNotify(ctx context.Context, ids []string, con q := self.Query() if !group { + if v := ctx.Value("uname"); v != nil { + ids, err = self._UIDsFromUIDOrName(ids) + if err != nil { + return nil, errors.Wrap(err, "fail to transfer array of UID or Uname to UIDs") + } + } uids = ids } else { uid := make([]string, 0) @@ -225,6 +268,7 @@ func (self *SContactManager) GetAllNotify(ctx context.Context, ids []string, con } uid = append(uid, tmpUids...) } + uids = uid } q.Filter(sqlchemy.AND(sqlchemy.In(q.Field("uid"), uids), sqlchemy.Equals(q.Field("contact_type"), contactType), sqlchemy.Equals(q.Field("status"), CONTACT_VERIFIED))) diff --git a/pkg/notify/models/mod_notification.go b/pkg/notify/models/mod_notification.go index 25b526c8be..bd48f9b745 100644 --- a/pkg/notify/models/mod_notification.go +++ b/pkg/notify/models/mod_notification.go @@ -468,11 +468,12 @@ func sendWithoutUserCred(notifications []SNotification) { wg.Wait() } -func ReSend(minutes int) { - scope := time.Duration(minutes) * time.Minute +func ReSend(seconds int) { + scope := time.Duration(seconds) * time.Second notifications, err := NotificationManager.FetchNotOK(time.Now().Add(-scope)) if err != nil { return } + log.Debugf("Start to resend message with a total of %d", len(notifications)) sendWithoutUserCred(notifications) } diff --git a/pkg/notify/models/mod_template.go b/pkg/notify/models/mod_template.go new file mode 100644 index 0000000000..3d39979938 --- /dev/null +++ b/pkg/notify/models/mod_template.go @@ -0,0 +1,198 @@ +// 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" + "database/sql" + "encoding/json" + "io/ioutil" + "os" + "strings" + "sync" + ptem "text/template" + + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/notify/rpc/apis" + "yunion.io/x/onecloud/pkg/notify/template" +) + +type STemplateManager struct { + SStandaloneResourceBaseManager +} + +var TemplateManager *STemplateManager + +func init() { + TemplateManager = &STemplateManager{ + SStandaloneResourceBaseManager: NewStandaloneResourceBaseManager( + STemplate{}, + "notify_t_template", + "notifytemplate", + "notifytemplates", + ), + } +} + +const ( + TEMPLATE_TYPE_TITLE = "title" + TEMPLATE_TYPE_CONTENT = "content" + TEMPLATE_TYPE_REMOTE = "remote" +) + +var ( + DefaultEmailUrl = "" + EmailUrl = "" + EmailUrlLock sync.RWMutex +) + +type STemplate struct { + SStandaloneResourceBase + + ContactType string `width:"16" nullable:"false" create:"required" update:"user" list:"user"` + Topic string `width:"20" nullable:"false" create:"required" update:"user" list:"user"` + + // title | content | remote + TemplateType string `width:"10" nullable:"false" create:"required" update:"user" list:"user"` + Content string `length:"text" nullable:"false" create:"required" get:"user" list:"user"` +} + +func (tm *STemplateManager) GetEmailUrl() string { + EmailUrlLock.RLock() + defer EmailUrlLock.RUnlock() + if len(EmailUrl) == 0 { + return DefaultEmailUrl + } + return EmailUrl +} + +func (tm *STemplateManager) SetEmailUrl(url string) { + EmailUrlLock.Lock() + defer EmailUrlLock.Unlock() + EmailUrl = url +} + +func (tm *STemplateManager) InitializeData() error { + q := tm.Query().Equals("contact_type", "email").Equals("topic", "VERIFY").Equals("template_type", "content") + count, _ := q.CountWithError() + if count > 0 { + return nil + } + content, err := ioutil.ReadFile(template.EMAIL_VERIFY_CONTENT_PATH) + if os.IsNotExist(err) { + return nil + } + if err != nil { + return errors.Wrapf(err, "os.Open for '%s'", template.EMAIL_VERIFY_CONTENT_PATH) + } + contentTem := STemplate{ + ContactType: "email", + Topic: "VERIFY", + TemplateType: "content", + Content: string(content), + } + titleTem := STemplate{ + ContactType: "email", + Topic: "VERIFY", + TemplateType: "title", + Content: template.EMAIL_VERIFY_TITLE, + } + err = tm.TableSpec().InsertOrUpdate(&contentTem) + if err != nil { + return errors.Wrap(err, "sqlchemy.TableSpec.InsertOrUpdate") + } + tm.TableSpec().InsertOrUpdate(&titleTem) + if err != nil { + return errors.Wrap(err, "sqlchemy.TableSpec.InsertOrUpdate") + } + return nil +} + +// NotifyFilter will return the title and content generated by corresponding template. +// Local cache about common template will be considered in case of performance issues. +func (tm *STemplateManager) NotifyFilter(contactType, topic, msg string) (params apis.SendParams, err error) { + params.Topic = topic + templates := make([]STemplate, 0, 3) + q := tm.Query().Equals("contact_type", contactType).Equals("topic", strings.ToUpper(topic)) + err = db.FetchModelObjects(tm, q, &templates) + if errors.Cause(err) == sql.ErrNoRows { + // no such template, return as is + params.Title = topic + params.Message = msg + return + } + if err != nil { + err = errors.Wrap(err, "db.FetchModelObjects") + return + } + for _, template := range templates { + var title, content string + switch template.TemplateType { + case TEMPLATE_TYPE_TITLE: + title, err = template.Execute(msg) + if err != nil { + return + } + params.Title = title + case TEMPLATE_TYPE_CONTENT: + content, err = template.Execute(msg) + if err != nil { + return + } + params.Message = content + case TEMPLATE_TYPE_REMOTE: + params.RemoteTemplate = template.Content + params.Message = msg + default: + err = errors.Error("no support template type") + return + } + } + return +} + +func (tm *STemplate) Execute(str string) (string, error) { + tem, err := ptem.New("tmp").Parse(tm.Content) + if err != nil { + return "", errors.Wrapf(err, "Template.Parse for template %s", tm.GetId()) + } + var buffer bytes.Buffer + tmpMap := make(map[string]interface{}) + err = json.Unmarshal([]byte(str), &tmpMap) + if err != nil { + return "", errors.Wrap(err, "json.Unmarshal") + } + err = tem.Execute(&buffer, tmpMap) + if err != nil { + return "", errors.Wrap(err, "template,Execute") + } + return buffer.String(), nil +} + +func (manager *STemplateManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, + ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data *jsonutils.JSONDict) (*jsonutils.JSONDict, error) { + + ty, _ := data.GetString("template_type") + if ty != TEMPLATE_TYPE_TITLE && ty != TEMPLATE_TYPE_CONTENT && ty != TEMPLATE_TYPE_REMOTE { + return nil, httperrors.NewInputParameterError("no such support for tempalte type %s", ty) + } + return data, nil +} diff --git a/pkg/notify/models/worker.go b/pkg/notify/models/worker.go index 6b40673d0b..42403cd1fb 100644 --- a/pkg/notify/models/worker.go +++ b/pkg/notify/models/worker.go @@ -25,8 +25,9 @@ import ( "yunion.io/x/onecloud/pkg/appsrv" "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/notifyclient" "yunion.io/x/onecloud/pkg/mcclient" - "yunion.io/x/onecloud/pkg/notify/options" + "yunion.io/x/onecloud/pkg/mcclient/modules/notify" "yunion.io/x/onecloud/pkg/notify/utils" ) @@ -54,7 +55,7 @@ func sendone(ctx context.Context, userCred mcclient.TokenCredential, notificatio err = NotifyService.Send(ctx, notification.ContactType, contact, notification.Topic, notification.Msg, notification.Priority) if err != nil { - log.Errorf("Send notification failed because that %s.", err.Error()) + log.Errorf("Send notification failed: %s.", err.Error()) notification.SetStatus(userCred, NOTIFY_FAIL, err.Error()) } else { log.Debugf("send notification successfully") @@ -82,7 +83,7 @@ func sendVerifyMessage(ctx context.Context, userCred mcclient.TokenCredential, v ) processId, token := verify.ID, verify.Token if contactType == "email" { - emailUrl := strings.Replace(options.Options.VerifyEmailUrl, "{0}", processId, 1) + emailUrl := strings.Replace(TemplateManager.GetEmailUrl(), "{0}", processId, 1) emailUrl = strings.Replace(emailUrl, "{1}", token, 1) // get uName @@ -105,7 +106,10 @@ func sendVerifyMessage(ctx context.Context, userCred mcclient.TokenCredential, v err = NotifyService.Send(ctx, contactType, contact, "verify", msg, "") if err != nil { verify.SetStatus(userCred, VERIFICATION_SENT_FAIL, "") - log.Errorf("Send verify message failed because that %s.", err.Error()) + // notify the uid through the webconsole + notifyclient.RawNotify([]string{uid}, false, notify.NotifyByWebConsole, notify.NotifyPriorityCritical, + "Send Verify Message Failed", jsonutils.NewString(err.Error())) + log.Errorf("Send verify message failed: %s.", err.Error()) return } verify.SetStatus(userCred, VERIFICATION_SENT, "") diff --git a/pkg/notify/options/options.go b/pkg/notify/options/options.go index f92e069ece..500e645121 100644 --- a/pkg/notify/options/options.go +++ b/pkg/notify/options/options.go @@ -22,12 +22,13 @@ 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 `help:"url of verify email"` - ReSendScope int `help:"Resend all messages that have not been sent successfully within ReSendScope minutes"` - InitNotificationScope int `help:"initialize data of notification with in InitNotificationScope hours" default:"100"` + 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 `help:"url of verify email"` + ReSendScope int `help:"Resend all messages that have not been sent successfully within ReSendScope +seconds"` + InitNotificationScope int `help:"initialize data of notification with in InitNotificationScope hours" default:"100"` } var Options NotifyOption diff --git a/pkg/notify/rpc/apis/send_server.pb.go b/pkg/notify/rpc/apis/send_server.pb.go index 2b30129f9a..f1db91f6b1 100644 --- a/pkg/notify/rpc/apis/send_server.pb.go +++ b/pkg/notify/rpc/apis/send_server.pb.go @@ -40,10 +40,12 @@ var _ = math.Inf const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package type SendParams struct { - Contact string `protobuf:"bytes,1,opt,name=contact,proto3" json:"contact,omitempty"` - Topic string `protobuf:"bytes,2,opt,name=topic,proto3" json:"topic,omitempty"` - Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"` - Priority string `protobuf:"bytes,4,opt,name=Priority,proto3" json:"Priority,omitempty"` + Contact string `protobuf:"bytes,1,opt,name=Contact,proto3" json:"Contact,omitempty"` + Topic string `protobuf:"bytes,2,opt,name=Topic,proto3" json:"Topic,omitempty"` + Title string `protobuf:"bytes,3,opt,name=Title,proto3" json:"Title,omitempty"` + Message string `protobuf:"bytes,4,opt,name=Message,proto3" json:"Message,omitempty"` + Priority string `protobuf:"bytes,5,opt,name=Priority,proto3" json:"Priority,omitempty"` + RemoteTemplate string `protobuf:"bytes,6,opt,name=RemoteTemplate,proto3" json:"RemoteTemplate,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -88,6 +90,13 @@ func (m *SendParams) GetTopic() string { return "" } +func (m *SendParams) GetTitle() string { + if m != nil { + return m.Title + } + return "" +} + func (m *SendParams) GetMessage() string { if m != nil { return m.Message @@ -102,6 +111,13 @@ func (m *SendParams) GetPriority() string { return "" } +func (m *SendParams) GetRemoteTemplate() string { + if m != nil { + return m.RemoteTemplate + } + return "" +} + type UpdateConfigParams struct { Configs map[string]string `protobuf:"bytes,1,rep,name=configs,proto3" json:"configs,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` XXX_NoUnkeyedLiteral struct{} `json:"-"` @@ -262,28 +278,30 @@ func init() { func init() { proto.RegisterFile("send_server.proto", fileDescriptor_63fdd68f7eb311f9) } var fileDescriptor_63fdd68f7eb311f9 = []byte{ - // 325 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x92, 0xcd, 0x4a, 0x03, 0x31, - 0x10, 0xc7, 0xd9, 0x7e, 0xda, 0x69, 0x91, 0x1a, 0x8b, 0xc4, 0x3d, 0x95, 0x85, 0x42, 0x2f, 0xee, - 0xa1, 0x22, 0x48, 0x2f, 0xa2, 0x52, 0x3c, 0x09, 0xa5, 0xd2, 0xb3, 0xa4, 0xdd, 0xe9, 0x12, 0xec, - 0x6e, 0x96, 0x24, 0x2d, 0xec, 0x63, 0xf8, 0x2a, 0x3e, 0xa1, 0xe4, 0x63, 0xb5, 0xc5, 0x7a, 0xcb, - 0x6f, 0x66, 0xfe, 0x99, 0xf9, 0x4f, 0x02, 0x17, 0x0a, 0xf3, 0xe4, 0x5d, 0xa1, 0xdc, 0xa3, 0x8c, - 0x0b, 0x29, 0xb4, 0x20, 0x0d, 0x56, 0x70, 0x15, 0x49, 0x80, 0x37, 0xcc, 0x93, 0x39, 0x93, 0x2c, - 0x53, 0x84, 0x42, 0x7b, 0x2d, 0x72, 0xcd, 0xd6, 0x9a, 0x06, 0xc3, 0x60, 0xdc, 0x59, 0x54, 0x48, - 0x06, 0xd0, 0xd4, 0xa2, 0xe0, 0x6b, 0x5a, 0xb3, 0x71, 0x07, 0xa6, 0x3e, 0x43, 0xa5, 0x58, 0x8a, - 0xb4, 0xee, 0xea, 0x3d, 0x92, 0x10, 0xce, 0xe6, 0x92, 0x0b, 0xc9, 0x75, 0x49, 0x1b, 0x36, 0xf5, - 0xc3, 0xd1, 0x67, 0x00, 0x64, 0x59, 0x24, 0x4c, 0xe3, 0xb3, 0xc8, 0x37, 0x3c, 0xf5, 0xcd, 0x1f, - 0x6c, 0xf3, 0x0d, 0x4f, 0x15, 0x0d, 0x86, 0xf5, 0x71, 0x77, 0x32, 0x8a, 0xcd, 0x88, 0xf1, 0xdf, - 0xd2, 0xd8, 0x81, 0x9a, 0xe5, 0x5a, 0x96, 0x8b, 0x4a, 0x15, 0x4e, 0xa1, 0x77, 0x98, 0x20, 0x7d, - 0xa8, 0x7f, 0x60, 0xe9, 0x9d, 0x98, 0xa3, 0x71, 0xb1, 0x67, 0xdb, 0x1d, 0x56, 0x2e, 0x2c, 0x4c, - 0x6b, 0xf7, 0x41, 0x14, 0xc3, 0x60, 0xa9, 0x50, 0xf2, 0xe4, 0xa9, 0x7c, 0x15, 0x2b, 0xbe, 0x45, - 0x3f, 0xd4, 0x15, 0xb4, 0x32, 0xcb, 0xfe, 0x1a, 0x4f, 0x51, 0x1b, 0x9a, 0xb3, 0xac, 0xd0, 0x65, - 0x74, 0x03, 0x97, 0xc7, 0xc2, 0x05, 0x16, 0xdb, 0xd2, 0xe8, 0x76, 0x36, 0x5c, 0xe9, 0x1c, 0x4d, - 0xbe, 0x02, 0xe8, 0x98, 0x85, 0x3f, 0xa6, 0x98, 0x6b, 0x32, 0x82, 0x86, 0x01, 0xd2, 0x77, 0x4e, - 0x7f, 0x5f, 0x22, 0xec, 0xba, 0x88, 0xed, 0x41, 0xee, 0xa0, 0x77, 0xb8, 0x04, 0x42, 0xff, 0x5b, - 0xcc, 0xb1, 0xec, 0x05, 0xce, 0x8f, 0x47, 0x23, 0xa1, 0x17, 0x9e, 0x70, 0x1a, 0x5e, 0x9f, 0xca, - 0x59, 0x33, 0xab, 0x96, 0xfd, 0x31, 0xb7, 0xdf, 0x01, 0x00, 0x00, 0xff, 0xff, 0x49, 0x12, 0x30, - 0x1e, 0x46, 0x02, 0x00, 0x00, + // 357 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x92, 0xcf, 0x6a, 0xe3, 0x30, + 0x10, 0xc6, 0x71, 0xfe, 0x6e, 0x26, 0x21, 0x64, 0xb5, 0x61, 0xd1, 0xfa, 0x14, 0x0c, 0x59, 0x72, + 0x59, 0x1f, 0xb2, 0x2c, 0x2c, 0xb9, 0x94, 0x36, 0x84, 0x9e, 0x02, 0xc1, 0x4d, 0xce, 0x45, 0x89, + 0xa7, 0x41, 0xd4, 0xb6, 0x8c, 0xa4, 0x04, 0xfc, 0x18, 0x7d, 0x93, 0xd2, 0x27, 0x2c, 0x92, 0xe5, + 0x36, 0x69, 0xd3, 0x9b, 0x7f, 0xdf, 0xe8, 0x1b, 0xcf, 0x37, 0x12, 0x7c, 0x57, 0x98, 0xc5, 0xf7, + 0x0a, 0xe5, 0x11, 0x65, 0x98, 0x4b, 0xa1, 0x05, 0x69, 0xb0, 0x9c, 0xab, 0xe0, 0xd9, 0x03, 0xb8, + 0xc3, 0x2c, 0x5e, 0x31, 0xc9, 0x52, 0x45, 0x28, 0xb4, 0xe7, 0x22, 0xd3, 0x6c, 0xa7, 0xa9, 0x37, + 0xf2, 0x26, 0x9d, 0xa8, 0x42, 0x32, 0x84, 0xe6, 0x5a, 0xe4, 0x7c, 0x47, 0x6b, 0x56, 0x2f, 0xc1, + 0xaa, 0x5c, 0x27, 0x48, 0xeb, 0x4e, 0x35, 0x60, 0xba, 0x2c, 0x51, 0x29, 0xb6, 0x47, 0xda, 0x28, + 0xbb, 0x38, 0x24, 0x3e, 0x7c, 0x5b, 0x49, 0x2e, 0x24, 0xd7, 0x05, 0x6d, 0xda, 0xd2, 0x1b, 0x93, + 0xdf, 0xd0, 0x8f, 0x30, 0x15, 0x1a, 0xd7, 0x98, 0xe6, 0x09, 0xd3, 0x48, 0x5b, 0xf6, 0xc4, 0x07, + 0x35, 0x78, 0xf2, 0x80, 0x6c, 0xf2, 0x98, 0x69, 0x9c, 0x8b, 0xec, 0x81, 0xef, 0xdd, 0xe8, 0x57, + 0xd0, 0xde, 0x59, 0x56, 0xd4, 0x1b, 0xd5, 0x27, 0xdd, 0xe9, 0x38, 0x34, 0x09, 0xc3, 0xcf, 0x47, + 0xc3, 0x12, 0xd4, 0x22, 0xd3, 0xb2, 0x88, 0x2a, 0x97, 0x3f, 0x83, 0xde, 0x69, 0x81, 0x0c, 0xa0, + 0xfe, 0x88, 0x85, 0xdb, 0x83, 0xf9, 0x34, 0x69, 0x8f, 0x2c, 0x39, 0x60, 0xb5, 0x03, 0x0b, 0xb3, + 0xda, 0x7f, 0x2f, 0x08, 0x61, 0xb8, 0x51, 0x28, 0x79, 0x7c, 0x53, 0x2c, 0xc5, 0x96, 0x27, 0xe8, + 0x86, 0xfa, 0x09, 0xad, 0xd4, 0xb2, 0x6b, 0xe3, 0x28, 0x68, 0x43, 0x73, 0x91, 0xe6, 0xba, 0x08, + 0xfe, 0xc0, 0x8f, 0x73, 0x63, 0x84, 0x79, 0x52, 0x18, 0xdf, 0xc1, 0xca, 0x95, 0xaf, 0xa4, 0xe9, + 0x8b, 0x07, 0x1d, 0x73, 0x5d, 0xd7, 0x7b, 0xcc, 0x34, 0x19, 0x43, 0xc3, 0x00, 0x19, 0x94, 0x49, + 0xdf, 0xef, 0xd1, 0xef, 0x96, 0x8a, 0xfd, 0x07, 0xf9, 0x07, 0xbd, 0xd3, 0x25, 0x10, 0xfa, 0xd5, + 0x62, 0xce, 0x6d, 0xb7, 0xd0, 0x3f, 0x1f, 0x8d, 0xf8, 0xce, 0x78, 0x21, 0xa9, 0xff, 0xeb, 0x52, + 0xcd, 0x86, 0xd9, 0xb6, 0xec, 0x83, 0xfb, 0xfb, 0x1a, 0x00, 0x00, 0xff, 0xff, 0xd7, 0x6c, 0xcb, + 0x1b, 0x85, 0x02, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. diff --git a/pkg/notify/rpc/apis/send_server.proto b/pkg/notify/rpc/apis/send_server.proto index e2060ecfc8..c065347486 100644 --- a/pkg/notify/rpc/apis/send_server.proto +++ b/pkg/notify/rpc/apis/send_server.proto @@ -1,12 +1,28 @@ +// 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. + syntax = "proto3"; package apis; message SendParams { - string contact = 1; - string topic = 2; - string message = 3; - string Priority = 4; + string Contact = 1; + string Topic = 2; + string Title = 3; + string Message = 4; + string Priority = 5; + string RemoteTemplate = 6; } message UpdateConfigParams { diff --git a/pkg/notify/rpc/send.go b/pkg/notify/rpc/send.go index e1d9405a48..b6db8fe127 100644 --- a/pkg/notify/rpc/send.go +++ b/pkg/notify/rpc/send.go @@ -50,14 +50,17 @@ type SRpcService struct { SendServices *ServiceMap socketFileDir string configStore _interface.IServiceConfigStore + templateStore _interface.ITemplateStore } // NewSRpcService create a SRpcService -func NewSRpcService(socketFileDir string, configStore _interface.IServiceConfigStore) *SRpcService { +func NewSRpcService(socketFileDir string, configStore _interface.IServiceConfigStore, + tempalteStore _interface.ITemplateStore) *SRpcService { return &SRpcService{ SendServices: NewServiceMap(), socketFileDir: socketFileDir, configStore: configStore, + templateStore: tempalteStore, } } @@ -106,20 +109,24 @@ func (self *SRpcService) StopAll() { // Send call the corresponding rpc server to send messager. func (self *SRpcService) Send(ctx context.Context, contactType, contact, topic, msg, priority string) error { - args := apis.SendParams{ - Contact: contact, - Topic: topic, - Message: msg, - Priority: priority, + args, err := self.templateStore.NotifyFilter(contactType, topic, msg) + if err != nil { + return errors.Wrap(err, "templateStore.NotifyFilter") } + args.Contact = contact + args.Priority = priority + f := func(service *apis.SendNotificationClient) (interface{}, error) { log.Debugf("send one") return service.Send(ctx, &args) } - _, err := self.execute(ctx, f, contactType) - return err + _, err = self.execute(ctx, f, contactType) + if err != nil { + return errors.Wrapf(err, "contactType: %s", contactType) + } + return nil } // RestartService can restart remote rpc server and pass config info. @@ -179,7 +186,7 @@ func (self *SRpcService) execute(ctx context.Context, f func(client *apis.SendNo } if st.Message() != ErrSendServiceNotInit.Error() { - return nil, errors.Error(fmt.Sprintf("Send message failed because that %s.", st.Message())) + return nil, errors.Error(st.Message()) } // if NOINIT, try to restart server and send again @@ -197,7 +204,7 @@ func (self *SRpcService) execute(ctx context.Context, f func(client *apis.SendNo return nil, errors.Wrap(ErrSendServiceNotFound, serviceName) } - return nil, errors.Error(fmt.Sprintf("Send message failed because that %s.", st.Message())) + return nil, errors.Error(st.Message()) } } return ret, nil diff --git a/pkg/notify/service.go b/pkg/notify/service.go index 95c2ea8e72..7b40a57e01 100644 --- a/pkg/notify/service.go +++ b/pkg/notify/service.go @@ -43,6 +43,9 @@ func StartService() { baseOpts := &options.Options.BaseOptions common_options.ParseOptions(opts, os.Args, "notify.conf", "notify") + // init email url + models.TemplateManager.SetEmailUrl(options.Options.VerifyEmailUrl) + // init auth app.InitAuth(commonOpts, func() { log.Infof("Auth complete!") @@ -60,7 +63,7 @@ func StartService() { cache.RegistUserCredCacheUpdater() // init notify service - models.NotifyService = rpc.NewSRpcService(opts.SocketFileDir, models.ConfigManager) + models.NotifyService = rpc.NewSRpcService(opts.SocketFileDir, models.ConfigManager, models.TemplateManager) models.NotifyService.InitAll() defer models.NotifyService.StopAll() @@ -72,7 +75,8 @@ func StartService() { resend := func(ctx context.Context, userCred mcclient.TokenCredential, isStart bool) { models.ReSend(opts.ReSendScope) } - cron.AddJobAtIntervals("ReSendNotifications", time.Duration(opts.ReSendScope)*time.Minute, resend) + cron.AddJobAtIntervals("ReSendNotifications", time.Duration(opts.ReSendScope)*time.Second, resend) + cron.Start() app.ServeForever(applicaion, baseOpts) } diff --git a/pkg/notify/template/doc.go b/pkg/notify/template/doc.go new file mode 100644 index 0000000000..aebfaab473 --- /dev/null +++ b/pkg/notify/template/doc.go @@ -0,0 +1 @@ +package template // import "yunion.io/x/onecloud/pkg/notify/template" diff --git a/pkg/notify/template/template.go b/pkg/notify/template/template.go new file mode 100644 index 0000000000..2ecc8ab580 --- /dev/null +++ b/pkg/notify/template/template.go @@ -0,0 +1,20 @@ +// 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 template + +const ( + EMAIL_VERIFY_CONTENT_PATH = "/opt/yunion/share/notify/email_verify_template" + EMAIL_VERIFY_TITLE = "Yunion Verify" +) diff --git a/pkg/notify/utils/keystone.go b/pkg/notify/utils/keystone.go index d72c0703d1..5529906139 100644 --- a/pkg/notify/utils/keystone.go +++ b/pkg/notify/utils/keystone.go @@ -17,11 +17,26 @@ package utils import ( "context" + "yunion.io/x/pkg/errors" + "yunion.io/x/sqlchemy" + + "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/notify/cache" ) -func GetUserByID(ctx context.Context, id string) (*cache.SUser, error) { - return cache.UserCacheManager.FetchUserByID(ctx, id, false) +func GetUserByIDOrName(ctx context.Context, idStr string) (*cache.SUser, error) { + return cache.UserCacheManager.FetchUserByIDOrName(ctx, idStr) +} + +func GetUsersWithoutRemote(idStr []string) ([]cache.SUser, error) { + q := cache.UserCacheManager.Query() + q = q.Filter(sqlchemy.OR(sqlchemy.In(q.Field("id"), idStr), sqlchemy.In(q.Field("name"), idStr))) + users := make([]cache.SUser, 0, 1) + err := db.FetchModelObjects(cache.UserCacheManager, q, &users) + if err != nil { + return nil, errors.Wrapf(err, "fetch user cache failed") + } + return users, nil } func GetUserIdsLikeName(ctx context.Context, name string) ([]string, error) { @@ -49,7 +64,7 @@ func GetUsersByGroupID(ctx context.Context, gid string) ([]string, error) { } func GetUsernameByID(ctx context.Context, id string) (string, error) { - user, err := GetUserByID(ctx, id) + user, err := GetUserByIDOrName(ctx, id) if err != nil { return "", err }