Merge pull request #4103 from rainzm/feature/notify_template

feat(notify): Available uname in api & Unified template management
This commit is contained in:
yunion-ci-robot
2019-12-16 11:42:00 +08:00
committed by GitHub
26 changed files with 867 additions and 228 deletions
+1
View File
@@ -12,4 +12,5 @@ RUN apk update && \
RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
COPY ./build/notify/root/opt/ /opt/
ADD ./_output/bin/notify /opt/yunion/bin/notify
File diff suppressed because one or more lines are too long
+4 -11
View File
@@ -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
+3 -1
View File
@@ -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
+90
View File
@@ -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
})
}
+2 -2
View File
@@ -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")
+16
View File
@@ -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
)
@@ -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)
}
@@ -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
}
+14 -116
View File
@@ -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,
+98 -5
View File
@@ -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)
+130 -29
View File
@@ -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["<uid>"]; ok {
userDetail, err := utils.GetUserByIDOrName(ctx, uid)
if err != nil {
httperrors.NotFoundError(w, "Uid or Uname Not Found")
return
}
params["<uid>"] = 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/<uid>/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/<uid>", 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/<uid>/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/<id>", 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/<id>", 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/<type>", prefix, modelDispatcher.KeywordPlural()),
modelDispatcher.Filter(configGetHandler), metadata, "get_configs", tags)
middleware(configGetHandler), metadata, "get_configs", tags)
app.AddHandler2("DELETE",
fmt.Sprintf("%s/%s/<type>", 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/<type>", prefix, EMAIL_KEYWORDPLURAL),
modelDispatcher.Filter(emailConfigGetHandler), metadata, "", tags)
middleware(emailConfigGetHandler), metadata, "", tags)
app.AddHandler2("DELETE",
fmt.Sprintf("%s/%s/<type>", prefix, EMAIL_KEYWORDPLURAL),
modelDispatcher.Filter(emailConfigDeleteHandler), metadata, "", tags)
middleware(emailConfigDeleteHandler), metadata, "", tags)
app.AddHandler2("PUT",
fmt.Sprintf("%s/%s/<type>", 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/<type>", prefix, SMS_KEYWORDPLURAL),
modelDispatcher.Filter(smsConfigGetHandler), metadata, "", tags)
middleware(smsConfigGetHandler), metadata, "", tags)
app.AddHandler2("DELETE",
fmt.Sprintf("%s/%s/<type>", prefix, SMS_KEYWORDPLURAL),
modelDispatcher.Filter(smsConfigDeleteHandler), metadata, "", tags)
middleware(smsConfigDeleteHandler), metadata, "", tags)
app.AddHandler2("PUT",
fmt.Sprintf("%s/%s/<type>", 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/<ctype>/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["<ctype>"]
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["<uid>"]
_, 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
+5
View File
@@ -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
+1
View File
@@ -31,6 +31,7 @@ func InitDB() error {
VerifyManager,
NotificationManager,
ConfigManager,
TemplateManager,
} {
err := manager.InitializeData()
if err != nil {
+50 -6
View File
@@ -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)))
+3 -2
View File
@@ -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)
}
+198
View File
@@ -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
}
+8 -4
View File
@@ -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, "")
+7 -6
View File
@@ -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
+44 -26
View File
@@ -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.
+20 -4
View File
@@ -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 {
+17 -10
View File
@@ -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
+6 -2
View File
@@ -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)
}
+1
View File
@@ -0,0 +1 @@
package template // import "yunion.io/x/onecloud/pkg/notify/template"
+20
View File
@@ -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"
)
+18 -3
View File
@@ -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
}