notify module in Go finished.

This commit is contained in:
Rain
2019-07-23 20:53:36 +08:00
parent 830f0baef3
commit 0a75f97d68
27 changed files with 2727 additions and 9 deletions

View File

@@ -29,8 +29,8 @@ func init() {
*/
type ContactsUpdateOptions struct {
UID string `help:"The user you wanna add contact to (Keystone User ID)"`
CONTACTTYPE string `help:"The contact type" choices:"email|mobile"`
CONTACT string `help:"The contacts details mobile number or email address, if set it the empty str means delete"`
CONTACTTYPE string `help:"The contact type email|mobile" choices:"email|mobile|dingtalk"`
CONTACT string `help:"The contacts details mobile number or email address or dingtalk's userid, if set it the empty str means delete"`
Status string `help:"Enabled or disabled contact status" choices:"enable|disable"`
}
R(&ContactsUpdateOptions{}, "contact-update", "Create, delete or update contact for user", func(s *mcclient.ClientSession, args *ContactsUpdateOptions) error {
@@ -40,9 +40,9 @@ func init() {
tmpObj.Add(jsonutils.NewString(args.CONTACT), "contact")
if len(args.Status) > 0 {
if args.Status == "disable" {
tmpObj.Add(jsonutils.JSONFalse, "enabled")
tmpObj.Add(jsonutils.NewInt(0), "enabled")
} else {
tmpObj.Add(jsonutils.JSONTrue, "enabled")
tmpObj.Add(jsonutils.NewInt(1), "enabled")
}
}
@@ -63,7 +63,7 @@ func init() {
type ContactsDeleteOptions struct {
UID string `help:"The user you wanna add contact to (Keystone User ID)"`
CONTACTTYPE string `help:"The contact type" choices:"email|mobile"`
CONTACTTYPE string `help:"The contact type email|mobile|dingtalk" choices:"email|mobile|dingtalk"`
}
R(&ContactsDeleteOptions{}, "contact-delete", "Delete contact for user", func(s *mcclient.ClientSession, args *ContactsDeleteOptions) error {
arr := jsonutils.NewArray()

View File

@@ -93,7 +93,7 @@ func init() {
STATUS string `help:"Notification send status" choices:"sent_ok|send_fail"`
Remark string `help:"Remark or description of the operation or fail reason"`
}
R(&NotificationUpdateCallbackOptions{}, "notification-update-callback", "Update send status of the notification task", func(s *mcclient.ClientSession, args *NotificationUpdateCallbackOptions) error {
R(&NotificationUpdateCallbackOptions{}, "notification-update-callback", "UpdateItem send status of the notification task", func(s *mcclient.ClientSession, args *NotificationUpdateCallbackOptions) error {
params := jsonutils.NewDict()
params.Add(jsonutils.NewString(args.STATUS), "status")
if len(args.Remark) > 0 {

59
cmd/climc/shell/notify.go Normal file
View File

@@ -0,0 +1,59 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package shell
import (
"yunion.io/x/jsonutils"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/modules"
)
func init() {
type ConfigCreate2Options struct {
CONTACTTYPE string `help:"contact type (email|sms_aliyun or others)"`
CONFIGS []string `help:"config (k, v)"`
}
R(&ConfigCreate2Options{}, "notify-config-update", "config update, example: notify_config-update email mail.smtp.hostname hostname mail.smtp.hostport 123.", func(s *mcclient.ClientSession, args *ConfigCreate2Options) error {
tmp := jsonutils.NewDict()
for i := 0; i+1 < len(args.CONFIGS); i += 2 {
tmp.Add(jsonutils.NewString(args.CONFIGS[i+1]), args.CONFIGS[i])
}
body := jsonutils.NewDict()
body.Add(tmp, args.CONTACTTYPE)
modules.Configs.Create(s, body)
return nil
})
type ConfigGet2Options struct {
TYPE string `help:"contact type (email|sms_aliyun or others)"`
}
R(&ConfigGet2Options{}, "notify-config-show", "config show", func(s *mcclient.ClientSession, args *ConfigGet2Options) error {
result, err := modules.Configs.Get(s, args.TYPE, jsonutils.JSONNull)
if err != nil {
return err
}
printObject(result)
return nil
})
R(&ConfigGet2Options{}, "notify-config-delete", "config delete", func(s *mcclient.ClientSession, args *ConfigGet2Options) error {
result, err := modules.Configs.Delete(s, args.TYPE, jsonutils.JSONNull)
if err != nil {
return err
}
printObject(result)
return nil
})
}

9
cmd/notify/main.go Normal file
View File

@@ -0,0 +1,9 @@
package main
import (
"yunion.io/x/onecloud/pkg/notify"
)
func main() {
notify.StartService()
}

View File

@@ -3047,7 +3047,7 @@ func (self *SGuest) doSaveRenewInfo(ctx context.Context, userCred mcclient.Token
return nil
})
if err != nil {
log.Errorf("Update error %s", err)
log.Errorf("UpdateItem error %s", err)
return err
}
db.OpsLog.LogEvent(self, db.ACT_RENEW, self.GetShortDesc(ctx), userCred)

View File

@@ -404,4 +404,4 @@ func (self *SSnapshotPolicy) preCheck(
return nil, httperrors.NewNotFoundError("Disks %v not found", notFoundDisks)
}
return diskIds, nil
}
}

View File

@@ -13,7 +13,7 @@
// limitations under the License.
/**
* config.go - config file definitions
* mod_config.go - config file definitions
*
* @author Yaroslav Pogrebnyak <yyyaroslav@gmail.com>
* @author Gene Ponomarenko <kikomdev@gmail.com>

View File

@@ -0,0 +1,17 @@
package modules
type ConfigsManager struct {
ResourceManager
}
var (
Configs ConfigsManager
)
func init() {
Configs = ConfigsManager{NewNotifyManager("config", "configs",
[]string{},
[]string{})}
register(&Configs)
}

View File

@@ -0,0 +1,184 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notify
import (
"context"
"net/http"
"strconv"
"yunion.io/x/jsonutils"
"yunion.io/x/onecloud/pkg/appsrv"
"yunion.io/x/onecloud/pkg/httperrors"
)
const (
EMAIL_KEYWORDPLURAL = "email_configs"
EMAIL_KEYWORD = "email_config"
EMAIL = "email"
SMS_KEYWORDPLURAL = "sms_configs"
SMS_KEYWORD = "sms_config"
SMS = "mobile"
)
func emailConfigDeleteHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
// do not need modify
manager, _, _, _ := fetchEnv(ctx, w, r)
params := map[string]string{
"<type>": EMAIL,
}
err := manager.DeleteConfig(ctx, params)
if err != nil {
httperrors.GeneralServerError(w, err)
}
}
func emailConfigGetHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, _, query, _ := fetchEnv(ctx, w, r)
params := map[string]string{
"<type>": EMAIL,
}
ret, err := manager.GetConfig(ctx, params, query)
if err != nil {
httperrors.GeneralServerError(w, err)
}
// hostport should be int and ssl_global should be bool
newDataDict := make(map[string]interface{})
data, _ := ret.Get("config")
dataDict := data.(*jsonutils.JSONDict)
dataDict = database2Display(dataDict)
for _, k := range dataDict.SortedKeys() {
tmp, _ := dataDict.GetString(k)
switch k {
case "hostport":
port, _ := strconv.Atoi(tmp)
newDataDict[k] = port
case "ssl_global":
ssl, _ := strconv.ParseBool(tmp)
newDataDict[k] = ssl
default:
newDataDict[k] = tmp
}
}
appsrv.SendJSON(w, jsonutils.Marshal(map[string]map[string]interface{}{
EMAIL_KEYWORD: newDataDict,
}))
}
func dispaly2Database(dict *jsonutils.JSONDict) *jsonutils.JSONDict {
keys := dict.SortedKeys()
newKey := ""
for _, key := range keys {
switch key {
case "username", "password":
newKey = "mail." + key
case "hostname", "hostport":
newKey = "mail.smtp." + key
case "ssl_global":
newKey = "mail.global.ssl"
}
v, _ := dict.Get(key)
dict.Add(v, newKey)
dict.Remove(key)
}
return dict
}
func database2Display(dict *jsonutils.JSONDict) *jsonutils.JSONDict {
keys := dict.SortedKeys()
newKey := ""
for _, key := range keys {
switch key {
case "mail.username", "mail.password":
newKey = key[5:]
case "mail.smtp.hostname", "mail.smtp.hostport":
newKey = key[10:]
case "mail.global.ssl":
newKey = "ssl_global"
}
v, _ := dict.Get(key)
dict.Add(v, newKey)
dict.Remove(key)
}
return dict
}
func emailConfigUpdateHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, _, _, body := fetchEnv(ctx, w, r)
body, _ = body.Get(EMAIL_KEYWORD)
bodyRet := jsonutils.DeepCopy(body)
bodyDict := body.(*jsonutils.JSONDict)
newBody := jsonutils.NewDict()
newBody.Add(dispaly2Database(bodyDict), EMAIL)
err := manager.UpdateConfig(ctx, newBody)
if err != nil {
httperrors.GeneralServerError(w, err)
}
data := jsonutils.NewDict()
data.Add(jsonutils.NewInt(200), "code")
data.Add(jsonutils.NewString("OK"), "title")
data.Add(bodyRet, "message")
ret := jsonutils.NewDict()
ret.Add(data, EMAIL_KEYWORD)
appsrv.SendJSON(w, ret)
}
func smsConfigDeleteHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
// do not need modify
manager, _, _, _ := fetchEnv(ctx, w, r)
params := map[string]string{
"<type>": SMS,
}
err := manager.DeleteConfig(ctx, params)
if err != nil {
httperrors.GeneralServerError(w, err)
}
}
func smsConfigGetHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, _, query, _ := fetchEnv(ctx, w, r)
params := map[string]string{
"<type>": SMS,
}
ret, err := manager.GetConfig(ctx, params, query)
if err != nil {
httperrors.GeneralServerError(w, err)
}
// modify
data, _ := ret.Get("config")
appsrv.SendJSON(w, jsonutils.Marshal(map[string]jsonutils.JSONObject{
SMS_KEYWORD: data,
}))
}
func smsConfigUpdateHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, _, _, body := fetchEnv(ctx, w, r)
body, _ = body.Get(SMS_KEYWORD)
newBody := jsonutils.NewDict()
newBody.Add(body, SMS)
err := manager.UpdateConfig(ctx, newBody)
if err != nil {
httperrors.GeneralServerError(w, err)
}
data := jsonutils.NewDict()
data.Add(jsonutils.NewInt(200), "code")
data.Add(jsonutils.NewString("OK"), "title")
data.Add(body, "message")
ret := jsonutils.NewDict()
ret.Add(data, SMS_KEYWORD)
appsrv.SendJSON(w, ret)
}

513
pkg/notify/dispatcher.go Normal file
View File

@@ -0,0 +1,513 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notify
import (
"bytes"
"context"
"fmt"
"net/http"
"strings"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/utils"
"yunion.io/x/onecloud/pkg/appctx"
"yunion.io/x/onecloud/pkg/appsrv"
"yunion.io/x/onecloud/pkg/appsrv/dispatcher"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/cloudcommon/policy"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/notify/models"
"yunion.io/x/onecloud/pkg/util/rbacutils"
)
// NotifyModelDispatcher is designed to complete some function that db.DBModelDispatcher can't.
// The apis of notify module has a certain degree of particularity so that we can't use common function.
type NotifyModelDispatcher struct {
db.DBModelDispatcher
}
func NewNotifyModelDispatcher(manager db.IModelManager) *NotifyModelDispatcher {
return &NotifyModelDispatcher{*db.NewModelHandler(manager)}
}
func (self *NotifyModelDispatcher) GetConfig(ctx context.Context, params map[string]string, query jsonutils.JSONObject) (jsonutils.JSONObject, error) {
listResult, err := self.List(ctx, mergeQueryParams(params, query), nil)
if err != nil {
return nil, err
}
keyVs := make(map[string]string)
for _, ret := range listResult.Data {
key, _ := ret.GetString("key_text")
value, _ := ret.GetString("value_text")
keyVs[key] = value
}
return jsonutils.Marshal(map[string]map[string]string{
models.ConfigManager.Keyword(): keyVs,
}), nil
}
func (self *NotifyModelDispatcher) DeleteConfig(ctx context.Context, params map[string]string) error {
contactType := params["<type>"]
configs, err := models.ConfigManager.GetConfigByType(contactType)
if err != nil {
return errors.Wrap(err, "Get Config by contactType failed")
}
userCred := policy.FetchUserCredential(ctx)
for i := range configs {
err = DeleteItem(models.ConfigManager, &configs[i], ctx, userCred, jsonutils.JSONNull, jsonutils.JSONNull)
if err != nil {
return errors.Wrapf(err, "Delete part of old one, so please input new data again.")
}
}
return nil
}
// UpdateConfig update config and restart corresponding send service.
func (self *NotifyModelDispatcher) UpdateConfig(ctx context.Context, body jsonutils.JSONObject) error {
data := body.(*jsonutils.JSONDict)
contactType := data.SortedKeys()[0]
originData, err := models.ConfigManager.GetVauleByType(contactType)
if err != nil {
return err
}
tmp, _ := data.Get(contactType)
data = tmp.(*jsonutils.JSONDict)
userCred := policy.FetchUserCredential(ctx)
// If no config of type 'contactType' in database, create news.
// Else delete original ones and create news.
if len(originData) != 0 {
// delete original
configs, err := models.ConfigManager.GetConfigByType(contactType)
if err != nil {
return errors.Wrap(err, "Get Config by contactType failed")
}
for i := range configs {
err = DeleteItem(models.ConfigManager, &configs[i], ctx, userCred, jsonutils.JSONNull, jsonutils.JSONNull)
if err != nil {
return errors.Wrapf(err, "Delete part of old one, so please input new data again.")
}
}
}
// create
for _, key := range data.SortedKeys() {
createData := jsonutils.NewDict()
tmp, _ = data.Get(key)
createData.Add(tmp, "value_text")
createData.Add(jsonutils.NewString(key), "key_text")
createData.Add(jsonutils.NewString(contactType), "type")
_, err := self.Create(ctx, jsonutils.JSONNull, createData, nil)
if err != nil {
return errors.Wrapf(err, "Create config (%s, %s, %s) failed", contactType, key, tmp)
}
}
return nil
}
// CreateNotification create new notifications and send them through rpc.RpcService.
// If data contains 'gid' field, that means that send message to all users in group.
// Else send messager to user whose uid equals 'uid' in data.
func (self *NotifyModelDispatcher) CreateNotification(ctx context.Context, data jsonutils.JSONObject) (jsonutils.JSONObject, error) {
// Get all contacts info of group if data contains "gid".
// If no contact, return ErrContactNotFound.
contactType, _ := data.GetString("contact_type")
group, id := false, ""
if data.Contains("gid") {
group = true
id, _ = data.GetString("gid")
} else {
id, _ = data.GetString("uid")
}
contacts, err := models.ContactManager.GetAllNotify(id, contactType, group)
if err != nil {
return nil, httperrors.NewGeneralError(errors.Wrap(err, "get all contacts error"))
}
notificationIDs, err := models.NotificationManager.BatchCreate(ctx, data, contacts)
if err != nil {
return nil, httperrors.NewGeneralError(err)
}
ret := jsonutils.NewDict()
ret.Add(jsonutils.NewStringArray(notificationIDs), "notifications")
return ret, nil
}
// Verify process:
// 1.fetch verify by ID; 2.check that if verify is expired;
// 3.if not check that if token is correct and update status of contact whose id is verify's CID
// 4.otherwise generate a new verify and delete old one
func (self *NotifyModelDispatcher) Verify(ctx context.Context, params map[string]string, query jsonutils.JSONObject) error {
processID := params["<id>"]
token, _ := query.GetString("token")
manager := models.VerifyManager
verifys, err := manager.FetchByID(processID)
if err != nil {
return httperrors.NewGeneralError(err)
}
var verifition models.SVerify
if len(verifys) == 0 {
return httperrors.NewNotFoundError("%s verify record not found", processID)
}
current, have := time.Now(), false
for i := range verifys {
if current.Before(verifys[i].ExpireAt) {
verifition = verifys[i]
have = true
break
}
}
if !have {
return httperrors.NewBadRequestError(models.VERIFICATION_TOKEN_EXPIRED)
}
if verifition.Token != token {
return httperrors.NewBadRequestError(models.VERIFICATION_TOKEN_INVALID)
}
// modify contact's status and verified time.
data := jsonutils.NewDict()
data.Set("status", jsonutils.NewString(models.CONTACT_VERIFIED))
data.Set("verified_at", jsonutils.NewTimeString(current))
_, err = self.Update(ctx, verifition.CID, jsonutils.JSONNull, data, nil)
if err != nil {
return httperrors.NewGeneralError(err)
}
return nil
}
// VerifyTrigger process:
// 1.fetch contact by the information in data
// 2.if contact'status is 'init', make a new verify and send a verify message to the contact adress
// 3.if contact'status is 'verifying', fetch verify by CID, generate a new verify if it has expired
// or return a error mention that "please don't try again".
func (self *NotifyModelDispatcher) VerifyTrigger(ctx context.Context, params map[string]string, data jsonutils.JSONObject) (jsonutils.JSONObject, error) {
uid := params["<uid>"]
contact, _ := data.GetString("contact")
contactType, _ := data.GetString("contact_type")
contacts, err := models.ContactManager.FetchByMore(uid, contact, contactType)
if err != nil {
return nil, errors.Error(fmt.Sprintf(`uid %q don't have contact %q of contact_type %q'`, uid, contact, contactType))
}
userCred := policy.FetchUserCredential(ctx)
scontact := contacts[0]
makeNewVerify := func() (jsonutils.JSONObject, error) {
verification := models.NewSVerify(contactType, scontact.ID)
err = models.VerifyManager.Create(ctx, userCred, verification)
if err != nil {
return nil, httperrors.NewGeneralError(err)
}
// update contact state
updateDate := jsonutils.NewDict()
updateDate.Set("status", jsonutils.NewString(models.CONTACT_VERIFYING))
err = UpdateItem(models.ContactManager, &scontact, ctx, userCred, jsonutils.JSONNull, updateDate)
if err != nil {
return nil, httperrors.NewGeneralError(err)
}
processID := verification.ID
go models.SendVerifyMessage(processID, uid, contactType, contact, verification.Token)
ret := map[string]map[string]string{
"contact": {
"process_id": processID,
},
}
return jsonutils.Marshal(ret), nil
}
if scontact.Status == models.CONTACT_INIT {
return makeNewVerify()
}
if scontact.Status == models.CONTACT_VERIFYING {
if err != nil {
return nil, errors.Error(fmt.Sprintf(`uid %q don't have contact %q of contact_type %q`, uid, contact, contactType))
}
verifications, err := models.VerifyManager.FetchByCID(scontact.ID)
if err != nil {
return nil, httperrors.NewGeneralError(err)
}
current := time.Now()
for _, verification := range verifications {
if current.After(verification.ExpireAt) {
//delete old one
err = DeleteItem(models.VerifyManager, &verification, ctx, userCred, jsonutils.JSONNull, jsonutils.JSONNull)
if err != nil {
return nil, httperrors.NewGeneralError(err)
}
return makeNewVerify()
}
}
return nil, httperrors.NewGeneralError(models.ErrVeritying)
}
return jsonutils.JSONNull, nil
}
// DeleteContacts delete a group of contacts
func (self *NotifyModelDispatcher) DeleteContacts(ctx context.Context, uids2 []jsonutils.JSONObject) error {
// Get all id of uid
uids := make([]string, len(uids2))
for i := range uids2 {
uids[i] = strings.Trim(uids2[i].String(), `"`)
}
contacts, err := models.ContactManager.FetchByUIDs(uids)
if err != nil {
return httperrors.NewGeneralError(err)
}
userCred := policy.FetchUserCredential(ctx)
deleteFailed := make([]string, 0, 1)
for _, contact := range contacts {
err = DeleteItem(models.ContactManager, &contact, ctx, userCred, jsonutils.JSONNull, jsonutils.JSONNull)
if err != nil {
deleteFailed = append(deleteFailed, contact.ID)
}
}
if len(deleteFailed) != 0 {
errInfo := strings.Join(deleteFailed, ", ") + " ; these contact delete failed."
return errors.Error(errInfo)
}
return nil
}
// UpdateContacts analysis the data, update corresponding contacts if they exist in the database or create new ones.
func (self *NotifyModelDispatcher) UpdateContacts(ctx context.Context, idstr string, query jsonutils.JSONObject, data jsonutils.JSONObject, ctxIds []dispatcher.SResourceContext) (jsonutils.JSONObject, error) {
datas, err := data.GetArray("contacts")
if err != nil {
return nil, httperrors.NewGeneralError(errors.Wrapf(err, `"contacts" not found`))
}
type pair struct {
contact string
enabled string
}
// contactInfos will be used to find all contact info need to update.
// And others will be created.
contactInfos := make(map[string]pair)
contactTypes := make([]string, len(datas))
for i := range datas {
contactType, _ := datas[i].GetString("contact_type")
contact, _ := datas[i].GetString("contact")
enabled := "-1"
if datas[i].Contains("enabled") {
enabled, _ = datas[i].GetString("enabled")
}
contactInfos[contactType] = pair{contact, enabled}
contactTypes[i] = contactType
}
records, err := models.ContactManager.FetchByUIDAndCType(idstr, contactTypes)
if err != nil {
return nil, httperrors.NewGeneralError(err)
}
// updateFailed record the information of failed update
updateFailed := make([]string, 0, 1)
deleteFailed := make([]string, 0, 1)
// UpdateItem contact info
userCred := policy.FetchUserCredential(ctx)
for i := range records {
contactType := records[i].ContactType
pairUpdate := contactInfos[contactType]
if len(pairUpdate.contact) == 0 {
// delete
err = DeleteItem(models.ContactManager, &records[i], ctx, userCred, jsonutils.JSONNull, jsonutils.JSONNull)
if err != nil {
deleteFailed = append(deleteFailed, fmt.Sprintf(`uid:%q, contact_type:%q`, idstr, contactType))
}
continue
}
updateData := jsonutils.NewDict()
updateData.Set("contact", jsonutils.NewString(pairUpdate.contact))
if pairUpdate.enabled != "-1" {
updateData.Set("enabled", jsonutils.NewString(pairUpdate.enabled))
}
updateData.Set("status", jsonutils.NewString("init"))
err = UpdateItem(models.ContactManager, &records[i], ctx, userCred, jsonutils.JSONNull, updateData)
if err != nil {
updateFailed = append(updateFailed, fmt.Sprintf(`uid:%q, contact_type:%q, contact:%q`, idstr, contactType, pairUpdate.contact))
}
delete(contactInfos, contactType)
}
// createFailed record the information of failed creation
createFailed := make([]string, 0, 1)
// Create contact info
newDatas := make([]map[string]interface{}, 0, len(contactInfos))
for conType, conPair := range contactInfos {
tmpMap := map[string]interface{}{
"uid": idstr,
"contact_type": conType,
"contact": conPair.contact,
}
if conPair.enabled != "-1" {
tmpMap["enabled"] = conPair.enabled
}
// dingtalk don't need verify, judge and specified status for now
if conType == "dingtalk" {
tmpMap["status"] = models.CONTACT_VERIFIED
tmpMap["verified_at"] = time.Now()
}
newDatas = append(newDatas, tmpMap)
}
for _, newData := range newDatas {
_, err := self.Create(ctx, jsonutils.JSONNull, jsonutils.Marshal(newData), ctxIds)
if err != nil {
createFailed = append(createFailed, fmt.Sprintf(`uid:%q, contact:%q, contact_type:%q`, idstr, newData["contact_type"], newData["contact"]))
}
}
// generate error through updateFailed and createFailed
if len(updateFailed) != 0 || len(createFailed) != 0 || len(deleteFailed) != 0 {
var errInfoBuffer bytes.Buffer
if len(updateFailed) != 0 {
errInfoBuffer.WriteString(strings.Join(updateFailed, "; "))
errInfoBuffer.WriteString(" update failed. ")
}
if len(deleteFailed) != 0 {
errInfoBuffer.WriteString(strings.Join(updateFailed, "; "))
errInfoBuffer.WriteString(" delete failed. ")
}
if len(createFailed) != 0 {
errInfoBuffer.WriteString(strings.Join(createFailed, "; "))
errInfoBuffer.WriteString(" create failed. ")
}
errInfo := errInfoBuffer.String()
return nil, httperrors.NewGeneralError(errors.Error(errInfo))
}
return data, nil
}
// fetchEnv fetch handler, params, query and body from ctx(context.Context)
func fetchEnv(ctx context.Context, w http.ResponseWriter, r *http.Request) (*NotifyModelDispatcher, map[string]string, jsonutils.JSONObject, jsonutils.JSONObject) {
params, query, body := appsrv.FetchEnv(ctx, w, r)
metadata := appctx.AppContextMetadata(ctx)
manager, ok := metadata["manager"].(*NotifyModelDispatcher)
if !ok {
log.Fatalf("No manager found for URL: %s", r.URL)
}
return manager, params, query, body
}
func mergeQueryParams(params map[string]string, query jsonutils.JSONObject, excludes ...string) jsonutils.JSONObject {
if query == nil {
query = jsonutils.NewDict()
}
queryDict := query.(*jsonutils.JSONDict)
for k, v := range params {
if !utils.IsInStringArray(k, excludes) {
queryDict.Add(jsonutils.NewString(v), k[1:len(k)-1])
}
}
return queryDict
}
// DeleteItem delete a database record corresponding to model
func DeleteItem(manager db.IModelManager, model db.IModel, ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) error {
err := model.ValidateDeleteCondition(ctx)
if err != nil {
log.Errorf("validate delete condition error: %s", err)
return err
}
err = model.CustomizeDelete(ctx, userCred, query, data)
if err != nil {
log.Errorf("customize delete error: %s", err)
return httperrors.NewNotAcceptableError(err.Error())
}
model.PreDelete(ctx, userCred)
err = model.Delete(ctx, userCred)
if err != nil {
log.Errorf("Delete error %s", err)
return err
}
model.PostDelete(ctx, userCred)
return nil
}
// UpdateItem update a database record corresponding to model whose update fields are in data
func UpdateItem(manager db.IModelManager, item db.IModel, ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) error {
var err error
err = item.ValidateUpdateCondition(ctx)
if err != nil {
log.Errorf("validate update condition error: %s", err)
return httperrors.NewGeneralError(err)
}
dataDict, ok := data.(*jsonutils.JSONDict)
if !ok {
return httperrors.NewInternalServerError("Invalid data JSONObject")
}
dataDict, err = item.ValidateUpdateData(ctx, userCred, query, dataDict)
if err != nil {
errMsg := fmt.Sprintf("validate update data error: %s", err)
log.Errorf(errMsg)
return httperrors.NewGeneralError(err)
}
item.PreUpdate(ctx, userCred, query, dataDict)
diff, err := db.Update(item, func() error {
filterData := dataDict.CopyIncludes(updateFields(manager, userCred)...)
err = filterData.Unmarshal(item)
if err != nil {
errMsg := fmt.Sprintf("unmarshal fail: %s", err)
log.Errorf(errMsg)
return httperrors.NewGeneralError(err)
}
return nil
})
if err != nil {
log.Errorf("save update error: %s", err)
return httperrors.NewGeneralError(err)
}
db.OpsLog.LogEvent(item, db.ACT_UPDATE, diff, userCred)
item.PostUpdate(ctx, userCred, query, data)
return nil
}
// get the field of model which is d
func updateFields(manager db.IModelManager, userCred mcclient.TokenCredential) []string {
ret := make([]string, 0)
for _, col := range manager.TableSpec().Columns() {
tags := col.Tags()
update := tags["update"]
if allowAction(manager, userCred, update, db.IsAllowUpdate) {
ret = append(ret, col.Name())
}
}
return ret
}
func allowAction(manager db.IResource, userCred mcclient.TokenCredential, action string, testfunc func(scope rbacutils.TRbacScope, userCred mcclient.TokenCredential, manager db.IResource) bool) bool {
if action == "user" {
return true
}
if action == "domain" && (testfunc(rbacutils.ScopeDomain, userCred, manager) || testfunc(rbacutils.ScopeSystem, userCred, manager)) {
return true
}
if action == "admin" && testfunc(rbacutils.ScopeSystem, userCred, manager) {
return true
}
return false
}

349
pkg/notify/handlers.go Normal file
View File

@@ -0,0 +1,349 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notify
import (
"context"
"fmt"
"net/http"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/appsrv"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient/modules"
"yunion.io/x/onecloud/pkg/notify/models"
"yunion.io/x/onecloud/pkg/notify/utils"
)
func InitHandlers(app *appsrv.Application) {
db.RegisterModelManager(models.ContactManager)
db.RegisterModelManager(models.VerifyManager)
db.RegisterModelManager(models.NotificationManager)
db.RegisterModelManager(models.ConfigManager)
AddNotifyDispatcher("/api/v1/", app)
}
func AddNotifyDispatcher(prefix string, app *appsrv.Application) {
var metadata map[string]interface{}
var tags map[string]string
// Contact Handler
modelDispatcher := NewNotifyModelDispatcher(models.ContactManager)
metadata, tags = map[string]interface{}{"manager": modelDispatcher}, map[string]string{"resource": modelDispatcher.KeywordPlural()}
h := app.AddHandler2("POST",
fmt.Sprintf("%s/%s/<uid>/update-contact", prefix, modelDispatcher.KeywordPlural()),
modelDispatcher.Filter(contactUpdateHandler), metadata, "contact_update", tags)
modelDispatcher.CustomizeHandlerInfo(h)
// List
h = app.AddHandler2("GET",
fmt.Sprintf("%s/%s", prefix, modelDispatcher.KeywordPlural()),
modelDispatcher.Filter(listManyHandler), metadata, "list_contacts", tags)
modelDispatcher.CustomizeHandlerInfo(h)
h = app.AddHandler2("GET",
fmt.Sprintf("%s/%s/<uid>", prefix, modelDispatcher.KeywordPlural()),
modelDispatcher.Filter(listOneHandler), metadata, "list_by_uid", tags)
modelDispatcher.CustomizeHandlerInfo(h)
h = app.AddHandler2("POST",
fmt.Sprintf("%s/%s/delete-contact", prefix, modelDispatcher.KeywordPlural()),
modelDispatcher.Filter(deleteContactHandler), metadata, "delete", tags)
modelDispatcher.CustomizeHandlerInfo(h)
// verify-trigger
h = app.AddHandler2("POST",
fmt.Sprintf("%s/%s/<uid>/verify", prefix, modelDispatcher.KeywordPlural()),
modelDispatcher.Filter(verifyTriggerHandler), metadata, "verify_trigger", tags)
modelDispatcher.CustomizeHandlerInfo(h)
// Verify Handler, this modelDispatcher need db.DBModelDispatcher'Create function to create Contact so this modelDispatcher is
// NotifyModelDispatcher whose DBModelDispatcher has modelManager models.ContactManager
metadata, tags = map[string]interface{}{"manager": modelDispatcher}, map[string]string{"resource": models.VerifyManager.KeywordPlural()}
h = app.AddHandler2("GET",
fmt.Sprintf("%s/%s/<id>", prefix, models.VerifyManager.KeywordPlural()),
modelDispatcher.Filter(verifyHandler), metadata, "verify", tags)
// notification Handler
modelDispatcher = NewNotifyModelDispatcher(models.NotificationManager)
metadata, tags = map[string]interface{}{"manager": modelDispatcher}, map[string]string{"resource": modelDispatcher.KeywordPlural()}
h = app.AddHandler2("POST",
fmt.Sprintf("%s/%s/", prefix, modelDispatcher.KeywordPlural()),
modelDispatcher.Filter(notificationHandler), metadata, "send_notifications", tags)
modelDispatcher.CustomizeHandlerInfo(h)
h = app.AddHandler2("GET",
fmt.Sprintf("%s/%s/", prefix, modelDispatcher.KeywordPlural()),
modelDispatcher.Filter(listHandler), metadata, "send_notifications", tags)
modelDispatcher.CustomizeHandlerInfo(h)
h = app.AddHandler2("GET",
fmt.Sprintf("%s/%s/<id>", prefix, modelDispatcher.KeywordPlural()),
modelDispatcher.Filter(listHandler), metadata, "list_notification_by_id", tags)
modelDispatcher.CustomizeHandlerInfo(h)
// config Handler
modelDispatcher = NewNotifyModelDispatcher(models.ConfigManager)
metadata, tags = map[string]interface{}{"manager": modelDispatcher}, map[string]string{"resource": modelDispatcher.KeywordPlural()}
h = app.AddHandler2("POST",
fmt.Sprintf("%s/%s/", prefix, modelDispatcher.KeywordPlural()),
modelDispatcher.Filter(configUpdateHandler), metadata, "update_configs", tags)
modelDispatcher.CustomizeHandlerInfo(h)
h = app.AddHandler2("GET",
fmt.Sprintf("%s/%s/<type>", prefix, modelDispatcher.KeywordPlural()),
modelDispatcher.Filter(configGetHandler), metadata, "get_configs", tags)
modelDispatcher.CustomizeHandlerInfo(h)
h = app.AddHandler2("DELETE",
fmt.Sprintf("%s/%s/<type>", prefix, modelDispatcher.KeywordPlural()),
modelDispatcher.Filter(configDeleteHandler), metadata, "delete_configs", tags)
modelDispatcher.CustomizeHandlerInfo(h)
// email handler for being compatible
h = app.AddHandler2("POST",
fmt.Sprintf("%s/%s/", prefix, EMAIL_KEYWORDPLURAL),
modelDispatcher.Filter(emailConfigUpdateHandler), metadata, "", tags)
modelDispatcher.CustomizeHandlerInfo(h)
h = app.AddHandler2("GET",
fmt.Sprintf("%s/%s/<type>", prefix, EMAIL_KEYWORDPLURAL),
modelDispatcher.Filter(emailConfigGetHandler), metadata, "", tags)
modelDispatcher.CustomizeHandlerInfo(h)
h = app.AddHandler2("DELETE",
fmt.Sprintf("%s/%s/<type>", prefix, EMAIL_KEYWORDPLURAL),
modelDispatcher.Filter(emailConfigDeleteHandler), metadata, "", tags)
modelDispatcher.CustomizeHandlerInfo(h)
h = app.AddHandler2("PUT",
fmt.Sprintf("%s/%s/<type>", prefix, EMAIL_KEYWORDPLURAL),
modelDispatcher.Filter(emailConfigUpdateHandler), metadata, "", tags)
modelDispatcher.CustomizeHandlerInfo(h)
h = app.AddHandler2("POST",
fmt.Sprintf("%s/%s/", prefix, SMS_KEYWORDPLURAL),
modelDispatcher.Filter(smsConfigUpdateHandler), metadata, "", tags)
modelDispatcher.CustomizeHandlerInfo(h)
h = app.AddHandler2("GET",
fmt.Sprintf("%s/%s/<type>", prefix, SMS_KEYWORDPLURAL),
modelDispatcher.Filter(smsConfigGetHandler), metadata, "", tags)
modelDispatcher.CustomizeHandlerInfo(h)
h = app.AddHandler2("DELETE",
fmt.Sprintf("%s/%s/<type>", prefix, SMS_KEYWORDPLURAL),
modelDispatcher.Filter(smsConfigDeleteHandler), metadata, "", tags)
modelDispatcher.CustomizeHandlerInfo(h)
h = app.AddHandler2("PUT",
fmt.Sprintf("%s/%s/<type>", prefix, SMS_KEYWORDPLURAL),
modelDispatcher.Filter(smsConfigUpdateHandler), metadata, "", tags)
modelDispatcher.CustomizeHandlerInfo(h)
}
func configDeleteHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, params, _, _ := fetchEnv(ctx, w, r)
err := manager.DeleteConfig(ctx, params)
if err != nil {
httperrors.GeneralServerError(w, err)
}
}
func configGetHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, params, query, _ := fetchEnv(ctx, w, r)
ret, err := manager.GetConfig(ctx, params, query)
if err != nil {
httperrors.GeneralServerError(w, err)
}
appsrv.SendJSON(w, ret)
}
func configUpdateHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, _, _, body := fetchEnv(ctx, w, r)
if body.Contains("config") {
body, _ = body.Get("config")
}
if body.Contains("configs") {
body, _ = body.Get("config")
}
err := manager.UpdateConfig(ctx, body)
if err != nil {
httperrors.GeneralServerError(w, err)
}
}
func notificationHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, _, _, body := fetchEnv(ctx, w, r)
data, err := body.Get(manager.Keyword())
if err != nil {
httperrors.BadRequestError(w, "request body should contain %s", manager.Keyword())
}
ret, err := manager.CreateNotification(ctx, data)
if err != nil {
httperrors.GeneralServerError(w, err)
return
}
appsrv.SendJSON(w, ret)
}
// verify handler
func verifyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, params, query, _ := fetchEnv(ctx, w, r)
err := manager.Verify(ctx, params, query)
if err != nil {
httperrors.GeneralServerError(w, err)
}
}
// contact update handler
func contactUpdateHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, params, _, body := fetchEnv(ctx, w, r)
var data jsonutils.JSONObject
if body != nil {
if body.Contains(manager.Keyword()) {
data, _ = body.Get(manager.Keyword())
if data == nil {
data = body.(*jsonutils.JSONDict)
}
} else {
data = body
}
} else {
data = jsonutils.NewDict()
}
// check that if the uid is exist
uid := params["<uid>"]
_, err := utils.GetUserByID(uid)
if err != nil {
log.Errorf(`uid %q not found`, uid)
httperrors.NotFoundError(w, "Uid Not Found")
return
}
_, err = manager.UpdateContacts(ctx, uid, jsonutils.JSONNull, data, nil)
if err != nil {
log.Errorf(err.Error())
httperrors.BadRequestError(w, "")
return
}
return
}
// delete contact handler
func deleteContactHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, _, _, body := fetchEnv(ctx, w, r)
var data []jsonutils.JSONObject
var err error
if body != nil {
data, err = body.GetArray(manager.KeywordPlural())
if err != nil {
httperrors.BadRequestError(w, "request body should have %s", manager.KeywordPlural())
}
}
err = manager.DeleteContacts(ctx, data)
if err != nil {
log.Errorf("delete contact of %s failed, error: %s", data, err)
httperrors.GeneralServerError(w, errors.Error("delete failed"))
}
}
// verify trigger handler
func verifyTriggerHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, params, _, body := fetchEnv(ctx, w, r)
data, err := body.Get(models.ContactManager.Keyword())
if err != nil {
httperrors.BadRequestError(w, "request body should have %s", manager.KeywordPlural())
}
ret, err := manager.VerifyTrigger(ctx, params, data)
if err != nil {
log.Errorf("verifyTrigger failed beacause %s", err)
httperrors.GeneralServerError(w, err)
}
appsrv.SendJSON(w, ret)
}
//speciallist hander for contact records
func listManyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, params, query, _ := fetchEnv(ctx, w, r)
listResult, err := manager.List(ctx, mergeQueryParams(params, query), nil)
if err != nil {
httperrors.GeneralServerError(w, err)
return
}
listResult = arrangeList(listResult)
appsrv.SendJSON(w, modules.ListResult2JSONWithKey(listResult, manager.KeywordPlural()))
}
// list handler for all resource in notify module
func listHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, params, query, _ := fetchEnv(ctx, w, r)
listResult, err := manager.List(ctx, mergeQueryParams(params, query), nil)
if err != nil {
httperrors.GeneralServerError(w, err)
return
}
appsrv.SendJSON(w, modules.ListResult2JSONWithKey(listResult, manager.KeywordPlural()))
}
func listOneHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
manager, params, query, _ := fetchEnv(ctx, w, r)
listResult, err := manager.List(ctx, mergeQueryParams(params, query), nil)
if err != nil {
httperrors.GeneralServerError(w, err)
return
}
appsrv.SendJSON(w, wrap(arrangeOne(listResult), manager.Keyword()))
}
func wrap(data jsonutils.JSONObject, key string) jsonutils.JSONObject {
ret := jsonutils.NewDict()
ret.Add(data, key)
return ret
}
// For limit option, there is a bug but don't fix it for now.
// This limit point to contact record, but these contact records whose uid are same
// are considered as one record.
func arrangeList(listResult *modules.ListResult) *modules.ListResult {
ret := make(map[string]*jsonutils.JSONArray)
for _, data := range listResult.Data {
uid, _ := data.GetString("uid")
_, ok := ret[uid]
if !ok {
ret[uid] = jsonutils.NewArray()
}
ret[uid].Add(data)
}
data := make([]jsonutils.JSONObject, len(ret))
index := 0
for uid, value := range ret {
cr := models.NewSContactResponse(uid, value.String())
data[index] = jsonutils.Marshal(cr)
index++
}
listResult.Data = data
listResult.Total = len(ret)
return listResult
}
func arrangeOne(listResult *modules.ListResult) jsonutils.JSONObject {
if len(listResult.Data) == 0 {
return jsonutils.NewDict()
}
uid, _ := listResult.Data[0].GetString("uid")
details := jsonutils.NewArray()
for _, data := range listResult.Data {
details.Add(data)
}
return jsonutils.Marshal(models.NewSContactResponse(uid, details.String()))
}

90
pkg/notify/models/base.go Normal file
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 models
import (
"context"
"fmt"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
)
type SResourceBase struct {
db.SResourceBase
CreateBy string `width:"128" charset:"ascii" nullable:"true" create:"optional"`
UpdateBy string `width:"128" charset:"ascii" nullable:"true" update:"user"`
DeleteBy string `width:"128" charset:"ascii" nullable:"true"`
Remark jsonutils.JSONObject `get:"user"`
}
type SResourceBaseManager struct {
db.SResourceBaseManager
}
type IResourceBaseModel interface {
db.IModel
GetIResourceBaseModel() IResourceBaseModel
SetDeleteBy(string)
}
func NewResourceBaseManager(dt interface{}, tableName string, keyword string, keywordPlural string) SResourceBaseManager {
return SResourceBaseManager{db.NewResourceBaseManager(dt, tableName, keyword, keywordPlural)}
}
func (self *SResourceBase) GetIResourceBaseModel() IResourceBaseModel {
return self.GetVirtualObject().(IResourceBaseModel)
}
func (self *SResourceBase) SetDeleteBy(uid string) {
self.DeleteBy = uid
}
func (self *SResourceBaseManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data *jsonutils.JSONDict) (*jsonutils.JSONDict, error) {
data, err := self.SResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, data)
if err != nil {
return nil, err
}
data.Set("create_by", jsonutils.NewString(userCred.GetUserId()))
return data, nil
}
func (self *SResourceBase) PreUpdate(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) {
self.SResourceBase.PreUpdate(ctx, userCred, query, data)
self.UpdateBy = userCred.GetUserId()
}
func (self *SResourceBase) Delete(ctx context.Context, userCred mcclient.TokenCredential) error {
item := self.GetIResourceBaseModel()
_, err := db.Update(item, func() error {
item.SetDeleteBy(userCred.GetUserId())
return item.MarkDelete()
})
if err != nil {
msg := fmt.Sprintf("save update error %s", err)
log.Errorf(msg)
return httperrors.NewGeneralError(err)
}
if userCred != nil {
db.OpsLog.LogEvent(self, db.ACT_DELETE, self.GetShortDesc(ctx), userCred)
}
return nil
}

View File

@@ -0,0 +1,46 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package models
const (
EMAIL = "email"
MOBILE = "mobile"
DINGTALK = "dingtalk"
WEBCONSOLE = "webconsole"
// Received a task about sending a notification
NOTIFY_RECEIVED = "received"
// Nofity module hasn't sent the notification
NOTIFY_UNSENT = "unsent"
// Nofity module has sent notification, but result unkown
NOTIFY_SENT = "sent"
// Notification was sent successfully
NOTIFY_OK = "sent_ok"
// That sent a notification is failed
NOTIFY_FAIL = "sent_fail"
// Contact's status is init which means no verifying
CONTACT_INIT = "init"
// Contact's status is verifying
CONTACT_VERIFYING = "verifying"
// Contact's status is verified
CONTACT_VERIFIED = "verified"
// Verification was sent
VERIFICATION_SENT = "sent"
// Verification was verified
VERIFICATION_VERIFIED = "verified"
VERIFICATION_TOKEN_EXPIRED = "Verification code expired"
VERIFICATION_TOKEN_INVALID = "Incorrect verification code"
)

View File

@@ -0,0 +1,26 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package models
import "errors"
var (
ErrContactNotFound = errors.New("Contact Not Found")
ErrVeritying = errors.New("During the verification process, please do not try again")
ErrDial = errors.New("Dial Failed")
ErrGetConfig = errors.New("Get Config Failed")
ErrUpdateConfig = errors.New("Update Config Failed")
)

View File

@@ -0,0 +1,27 @@
package models
import (
"yunion.io/x/log"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
)
func InitDB() error {
for _, manager := range []db.IModelManager{
/*
* Important!!!
* initialization order matters, do not change the order
*/
ContactManager,
VerifyManager,
NotificationManager,
ConfigManager,
} {
err := manager.InitializeData()
if err != nil {
log.Errorf("Manager %s initializeData fail %s", manager.Keyword(), err)
// return err skip error table
}
}
return nil
}

View File

@@ -0,0 +1,119 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package models
import (
"context"
"fmt"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/errors"
"yunion.io/x/sqlchemy"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/mcclient"
)
type SConfigManager struct {
SStatusStandaloneResourceBaseManager
}
var ConfigManager *SConfigManager
func init() {
ConfigManager = &SConfigManager{
SStatusStandaloneResourceBaseManager: NewStatusStandaloneResourceBaseManager(
SConfig{},
"notify_t_config",
"config",
"configs",
),
}
ConfigManager.SetVirtualObject(ConfigManager)
}
// SConfig is a table which storage (k,v) and its type.
// The three important concepts are key, value and type.
// Key and type uniquely identify a value.
type SConfig struct {
SStatusStandaloneResourceBase
Type string `width:"15" nullable:"false" create:"required" list:"user""`
KeyText string `width:"30" nullable:"false" create:"required" list:"user"`
ValueText string `width:"100" nullable:"false" create:"required" list:"user"`
}
// ListItemFilter is a hook function belong to IModelManager interface when Listing.
// This will Called in yunion.io/x/onecloud/pkg/cloudcommon/db.List function.
func (self *SConfigManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (*sqlchemy.SQuery, error) {
if !query.Contains("type") {
return q, nil
}
contactType, _ := query.GetString("type")
q.Filter(sqlchemy.Equals(q.Field("type"), contactType))
return q, nil
}
// GetValue fetch the SConfig struct corresponding to key and type.
func (self *SConfigManager) GetValue(key, contactType string) (*SConfig, error) {
q := self.Query()
q.Filter(sqlchemy.AND(sqlchemy.Equals(q.Field("type"), contactType), sqlchemy.Equals(q.Field("key_text"), key)))
configs := make([]SConfig, 0, 1)
err := db.FetchModelObjects(self, q, &configs)
if err != nil {
return nil, errors.Wrap(err, "Fetch SConfig by key and type failed")
}
if len(configs) == 0 {
return nil, errors.Error("There is no SConfig whose key and type meet the requirement")
}
return &configs[0], nil
}
// Get all (k, v) whose type is contactType.
func (self *SConfigManager) GetVauleByType(contactType string) (map[string]string, error) {
configs, err := self.GetConfigByType(contactType)
if err != nil {
return nil, err
}
ret := make(map[string]string)
for i := range configs {
ret[configs[i].KeyText] = configs[i].ValueText
}
return ret, nil
}
func (self *SConfigManager) InitializeData() error {
sql := fmt.Sprintf("update %s set updated_at=gmt_modified, deleted=is_deleted, created_at=gmt_create, deleted_at=gmt_deleted, update_by=modified_by, delete_by=deleted_by", self.TableSpec().Name())
q := sqlchemy.NewRawQuery(sql, "")
q.Row()
sql = fmt.Sprintf("update %s set type='mobile' where type='sms_aliyun'", self.TableSpec().Name())
q = sqlchemy.NewRawQuery(sql, "")
q.Row()
return nil
}
// Fetch all SConfig struct which type is contactType.
func (self *SConfigManager) GetConfigByType(contactType string) ([]SConfig, error) {
q := self.Query()
q.Filter(sqlchemy.Equals(q.Field("type"), contactType))
configs := make([]SConfig, 0, 5)
err := db.FetchModelObjects(self, q, &configs)
if err != nil {
return nil, errors.Wrap(err, "Fetch SConfigs by type failed")
}
//if len(configs) == 0 {
// return nil, errors.Error("There is no SConfig whose type meet the requirement")
//}
return configs, nil
}

View File

@@ -0,0 +1,166 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package models
import (
"context"
"fmt"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/sqlchemy"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/notify/utils"
)
type SContactManager struct {
SStatusStandaloneResourceBaseManager
}
var ContactManager *SContactManager
func init() {
ContactManager = &SContactManager{
SStatusStandaloneResourceBaseManager: NewStatusStandaloneResourceBaseManager(
SContact{},
"notify_t_contacts",
"contact",
"contacts",
),
}
ContactManager.SetVirtualObject(ContactManager)
}
type SContact struct {
SStatusStandaloneResourceBase
UID string `width:"128" nullable:"false" create:"required" list:"user" update:"user"`
ContactType string `width:"16" nullable:"false" create:"required" list:"user" update:"user"`
Contact string `width:"64" nullable:"false" create:"required" list:"user" update:"user"`
Enabled string `width:"5" nullable:"false" default:"1" create:"optional" list:"user" update:"user"`
VerifiedAt time.Time `update:"user" list:"user"`
}
func (self *SContactManager) AllowListItems(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) bool {
return db.IsAdminAllowList(userCred, self)
}
func (self *SContactManager) AllowCreateItem(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool {
return true
}
func (self *SContactManager) InitializeData() error {
sql := fmt.Sprintf("update %s set updated_at=update_at, deleted=is_deleted", self.TableSpec().Name())
q := sqlchemy.NewRawQuery(sql, "")
q.Row()
return nil
}
func (self *SContactManager) FetchByUIDs(uids []string) ([]SContact, error) {
q := self.Query()
q = q.Filter(sqlchemy.In(q.Field("uid"), uids))
records := make([]SContact, 0, len(uids))
err := db.FetchModelObjects(self, q, &records)
if err != nil {
return nil, err
}
return records, nil
}
func (self *SContactManager) FetchByUIDAndCType(uid string, contactTypes []string) ([]SContact, error) {
q := self.Query("id", "uid", "contact_type", "contact", "enabled")
q = q.Filter(sqlchemy.AND(sqlchemy.Equals(q.Field("uid"), uid), sqlchemy.In(q.Field("contact_type"), contactTypes)))
records := make([]SContact, 0, len(contactTypes))
err := db.FetchModelObjects(self, q, &records)
if err != nil {
return nil, err
}
return records, nil
}
func (self *SContactManager) FetchByMore(uid, contact, contactType string) ([]SContact, error) {
q := self.Query()
q.Filter(sqlchemy.AND(sqlchemy.Equals(q.Field("uid"), uid), sqlchemy.Equals(q.Field("contact"), contact), sqlchemy.Equals(q.Field("contact_type"), contactType)))
records := make([]SContact, 0, 1)
err := db.FetchModelObjects(self, q, &records)
if err != nil {
return nil, err
}
return records, nil
}
func (self *SContactManager) FetchDingtalkContacts(uid string) {
// todo
}
func (self *SContactManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (*sqlchemy.SQuery, error) {
queryDict := query.(*jsonutils.JSONDict)
if queryDict.Contains("uid") {
uid, _ := queryDict.GetString("uid")
q = q.Filter(sqlchemy.Equals(q.Field("uid"), uid))
}
return q, nil
}
func (self *SContactManager) GetAllNotify(id, contactType string, group bool) ([]SContact, error) {
var uids []string
var err error
q := self.Query()
if !group {
q.Filter(sqlchemy.AND(sqlchemy.Equals(q.Field("uid"), id), sqlchemy.Equals(q.Field("contact_type"), contactType), sqlchemy.Equals(q.Field("status"), CONTACT_VERIFIED)))
uids = []string{id}
} else {
uids, err = utils.GetUsersByGroupID(id)
if err != nil {
return nil, err
}
q.Filter(sqlchemy.AND(sqlchemy.In(q.Field("uid"), uids), sqlchemy.Equals(q.Field("contact_type"), contactType)))
}
if contactType == WEBCONSOLE {
ret := make([]SContact, len(uids))
for i := range uids {
ret[i] = SContact{
UID: uids[i],
ContactType: WEBCONSOLE,
Contact: uids[i],
}
}
return ret, nil
}
contacts := make([]SContact, 0, 2)
err = db.FetchModelObjects(self, q, &contacts)
if err != nil {
return nil, err
}
return contacts, nil
}
type SContactResponse struct {
Id string
Name string
Details string
}
func NewSContactResponse(uid string, details string) SContactResponse {
name, _ := utils.GetUsernameByID(uid)
return SContactResponse{
Id: uid,
Name: name,
Details: details,
}
}

View File

@@ -0,0 +1,247 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package models
import (
"bytes"
"context"
"fmt"
"sync"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/sqlchemy"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/cloudcommon/policy"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/notify/utils"
)
type SNotificationManager struct {
SStatusStandaloneResourceBaseManager
}
var NotificationManager *SNotificationManager
func init() {
NotificationManager = &SNotificationManager{
SStatusStandaloneResourceBaseManager: NewStatusStandaloneResourceBaseManager(
SNotification{},
"notify_t_notification",
"notification",
"notifications",
),
}
NotificationManager.SetVirtualObject(NotificationManager)
}
type SNotification struct {
SStatusStandaloneResourceBase
UID string `width:"128" nullable:"false" create:"required"`
ContactType string `width:"16" nullable:"false" create:"required"`
Topic string `width:"128" nullable:"false" create:"optional"`
Priority string `width:"16" nullable:"false" create:"optional"`
Msg string `create:"required"`
ReceivedAt time.Time `nullable:"false"`
SendAt time.Time `nullable:"false"`
SendBy string `width:"128" nullable:"false"`
}
func (self *SNotificationManager) AllowListItems(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) bool {
return db.IsAdminAllowList(userCred, self)
}
func (self *SNotificationManager) AllowCreateItem(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool {
return db.IsAdminAllowCreate(userCred, self)
}
func (self *SNotificationManager) InitializeData() error {
sql := fmt.Sprintf("update %s set updated_at=update_at, deleted=is_deleted", self.TableSpec().Name())
q := sqlchemy.NewRawQuery(sql, "")
q.Row()
return nil
}
func (self *SNotificationManager) BatchCreate(ctx context.Context, data jsonutils.JSONObject, contacts []SContact) ([]string, error) {
userCred := policy.FetchUserCredential(ctx)
ownerID, err := utils.FetchOwnerId(ctx, NotificationManager, userCred, jsonutils.JSONNull)
if err != nil {
return nil, httperrors.NewGeneralError(err)
}
msg, _ := data.GetString("msg")
priority, _ := data.GetString("priority")
topic, _ := data.GetString("topic")
createFailed, createSuccess, contactSuccess := make([]string, 0), make([]*SNotification, 0, len(contacts)/2), make([]string, 0, len(contacts)/2)
for i := range contacts {
createData := map[string]string{
"uid": contacts[i].ID,
"contact_type": contacts[i].ContactType,
"topic": topic,
"priority": priority,
"msg": msg,
"send_by": userCred.GetUserId(),
"status": NOTIFY_UNSENT,
}
model, err := db.DoCreate(self, ctx, userCred, jsonutils.JSONNull, jsonutils.Marshal(createData), ownerID)
if err != nil {
createFailed = append(createFailed, contacts[i].ID)
} else {
createSuccess = append(createSuccess, model.(*SNotification))
contactSuccess = append(contactSuccess, contacts[i].Contact)
}
}
go send(createSuccess, userCred, contactSuccess)
if len(createFailed) != 0 {
errInfo := new(bytes.Buffer)
errInfo.WriteString("notifications whose uid are ")
for i := range createFailed {
errInfo.WriteString(createFailed[i])
errInfo.WriteString(", ")
}
errInfo.Truncate(errInfo.Len() - 2)
errInfo.WriteString("created failed.")
log.Errorf(errInfo.String())
return nil, errors.Error("Not all notifications were sent successfully")
}
notificationIDs := make([]string, len(createSuccess))
for i := range createSuccess {
notificationIDs[i] = createSuccess[i].ID
}
return notificationIDs, nil
}
func (self *SNotificationManager) FetchNotOK(lastTime time.Time) ([]SNotification, error) {
q := self.Query()
q.Filter(sqlchemy.AND(sqlchemy.GE(q.Field("created_at"), lastTime), sqlchemy.NotEquals(q.Field("status"), NOTIFY_UNSENT)))
records := make([]SNotification, 0, 10)
err := db.FetchModelObjects(self, q, &records)
if err != nil {
return nil, err
}
return records, nil
}
func send(notifications []*SNotification, userCred mcclient.TokenCredential, contacts []string) {
var wg sync.WaitGroup
sendone := func(notification *SNotification, contact string) {
err := notification.SetSentAndTime(userCred)
if err != nil {
log.Errorf("Change notification's status failed.")
return
}
err = RpcService.Send(notification.ContactType, contact, notification.Topic, notification.Msg, notification.Priority)
if err != nil {
log.Errorf("Send notification failed because that %s.", err.Error())
notification.SetStatus(userCred, NOTIFY_FAIL, err.Error())
} else {
notification.SetStatus(userCred, NOTIFY_OK, "")
}
wg.Done()
}
for i := range notifications {
wg.Add(1)
go sendone(notifications[i], contacts[i])
}
wg.Wait()
}
func (self *SNotification) SetSentAndTime(userCred mcclient.TokenCredential) error {
status := NOTIFY_SENT
if self.Status == status {
return nil
}
oldStatus := self.Status
_, err := db.Update(self, func() error {
self.Status = status
self.SendAt = time.Now()
return nil
})
if err != nil {
return err
}
reason := "sent notification"
if userCred != nil {
notes := fmt.Sprintf("%s=>%s", oldStatus, status)
if len(reason) > 0 {
notes = fmt.Sprintf("%s: %s", notes, reason)
}
db.OpsLog.LogEvent(self, db.ACT_UPDATE_STATUS, notes, userCred)
}
return nil
}
func (self *SNotification) SetStatusWithoutUserCred(status string) error {
_, err := db.Update(self, func() error {
self.Status = status
return nil
})
if err != nil {
return err
}
return nil
}
func sendWithoutUserCred(notifications []SNotification) {
var wg sync.WaitGroup
sendone := func(notification SNotification) {
// Get contact
contact, err := ContactManager.FetchByUIDAndCType(notification.UID, []string{notification.ContactType})
if err != nil {
return
}
if len(contact) == 0 {
return
}
// sent_at update todo
notification.SetStatusWithoutUserCred(NOTIFY_SENT)
err = RpcService.Send(notification.ContactType, contact[0].Contact, notification.Topic, notification.Msg, notification.Priority)
if err == nil {
return
}
if err != nil {
log.Errorf("Send notification failed because that %s.", err.Error())
notification.SetStatusWithoutUserCred(NOTIFY_FAIL)
} else {
notification.SetStatusWithoutUserCred(NOTIFY_OK)
}
wg.Done()
}
for i := range notifications {
wg.Add(1)
go sendone(notifications[i])
}
wg.Wait()
}
func ReSend(minutes int) {
scope := time.Duration(minutes) * time.Minute
for {
select {
case <-time.After(scope / 2):
//lastTime := time.Now().Add(-scope)
//q := NotificationManager.Query()
notifications, err := NotificationManager.FetchNotOK(time.Now().Add(-scope))
if err != nil {
break
}
sendWithoutUserCred(notifications)
}
}
}

View File

@@ -0,0 +1,153 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package models
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/sqlchemy"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/notify/options"
"yunion.io/x/onecloud/pkg/notify/utils"
)
type SVerifyManager struct {
SStatusStandaloneResourceBaseManager
}
var VerifyManager *SVerifyManager
func init() {
VerifyManager = &SVerifyManager{
SStatusStandaloneResourceBaseManager: NewStatusStandaloneResourceBaseManager(
SVerify{},
"notify_t_verify",
"verification",
"verifications",
),
}
VerifyManager.SetVirtualObject(VerifyManager)
}
type SVerify struct {
SStatusStandaloneResourceBase
CID string `width:"128" nullable:"false" create:"required" list:"user"`
Token string `width:"200" nullable:"false" create:"required" list:"user"`
SendAt time.Time `nullable:"false" create:"optional"`
ExpireAt time.Time `create:"required" list:"user"`
}
// NewSVerify Generate a SVerify instance which implement a Verification Token.
func NewSVerify(contactType string, cid string) *SVerify {
var token string
var expireAt time.Time
now := time.Now()
if contactType == EMAIL {
token = utils.GenerateEmailToken(32)
expireAt = now.Add(12 * time.Hour)
} else {
token = utils.GenerateMobileToken()
expireAt = now.Add(5 * time.Minute)
}
ret := &SVerify{
CID: cid,
Token: token,
ExpireAt: expireAt,
SendAt: now,
}
ret.ID = DefaultUUIDGenerator()
return ret
}
func (self *SVerifyManager) InitializeData() error {
sql := fmt.Sprintf("update %s set updated_at=update_at, deleted=is_deleted", self.TableSpec().Name())
q := sqlchemy.NewRawQuery(sql, "")
q.Row()
return nil
}
func (self *SVerifyManager) FetchByCID(cid string) ([]SVerify, error) {
q := self.Query()
q.Filter(sqlchemy.Equals(q.Field("cid"), cid))
records := make([]SVerify, 0, 1)
err := db.FetchModelObjects(self, q, &records)
if err != nil {
return nil, err
}
return records, nil
}
func (self *SVerifyManager) FetchByID(id string) ([]SVerify, error) {
q := self.Query()
q.Filter(sqlchemy.Equals(q.Field("id"), id))
records := make([]SVerify, 0, 1)
err := db.FetchModelObjects(self, q, &records)
if err != nil {
return nil, err
}
return records, nil
}
func (self *SVerifyManager) Create(ctx context.Context, userCred mcclient.TokenCredential, verify *SVerify) error {
data := jsonutils.Marshal(verify)
ownerID, err := utils.FetchOwnerId(ctx, self, userCred, data)
if err != nil {
return err
}
_, err = db.DoCreate(self, ctx, userCred, jsonutils.JSONNull, data, ownerID)
if err != nil {
return err
}
return nil
}
func SendVerifyMessage(processId, uid, contactType, contact, token string) {
var err error
var msg string
if contactType == "email" {
emailUrl := strings.Replace(options.Options.VerifyEmailUrl, "{0}", processId, 1)
emailUrl = strings.Replace(emailUrl, "{1}", token, 1)
// get uName
uName, err := utils.GetUsernameByID(uid)
if err != nil || len(uName) == 0 {
uName = "用户"
}
data := struct {
Name string
Link string
}{uName, emailUrl}
jsonStr, _ := json.Marshal(data)
msg = string(jsonStr)
} else if contactType == "mobile" {
msg = fmt.Sprintf(`{"code": "%s"}`, token)
} else {
//todo
}
err = RpcService.Send(contactType, contact, "verify", msg, "")
if err != nil {
log.Errorf("Send verify message failed because that %s.", err.Error())
}
}

269
pkg/notify/models/send.go Normal file
View File

@@ -0,0 +1,269 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package models
import (
"fmt"
"io/ioutil"
"net/rpc"
"os"
"path/filepath"
"strings"
"sync"
"time"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
)
const (
// ErrSendServiceNotFound means SRpcService's SendSerivces hasn't this Send Service.
ErrSendServiceNotFound = errors.Error("Send Service Not Found")
NOTINIT = "Send service hasn't been init"
)
// RpcService is a single case of SRpcService
var RpcService *SRpcService
// SRpcService provide rpc service about sending message for notify module and manage these services.
// SendServices storage all send service and its name.
// lock protect the SendServices.
type SRpcService struct {
SendServices map[string]*rpc.Client
socketFileDir string
lock sync.RWMutex
}
// NewSRpcService create a SRpcService
func NewSRpcService(socketFileDir string) *SRpcService {
return &SRpcService{
SendServices: make(map[string]*rpc.Client),
socketFileDir: socketFileDir,
}
}
// InitAll init all Send Services, the init process is that:
// find all socket file in directory 'self.socketFileDir', if wrong return error;
// the name of file is the service's name; then try to dial to this rpc service
// through corresponding socket file, if failed, only print log but not return error.
func (self *SRpcService) InitAll() error {
files, err := ioutil.ReadDir(self.socketFileDir)
if err != nil {
return errors.Wrapf(err, "read dir %s failed", self.socketFileDir)
}
for _, file := range files {
filename := file.Name()
if !file.IsDir() && strings.Contains(filename, ".sock") {
serviceName := filename[:len(filename)-5]
self.checkAndAddOne(serviceName)
}
}
if len(self.SendServices) == 0 {
log.Errorf("No available send service.")
} else {
log.Infof("Total %d send service init successful", len(self.SendServices))
}
return nil
}
// UpdateServices will detect the self.sockFileDir every delay seconds.
// Add new service and delete disappeared one from self.SendServices.
func (self *SRpcService) UpdateServices(delay int) {
for {
select {
case <-time.After(time.Duration(delay) * time.Second):
err := self.updateService()
if err != nil {
log.Errorf("update services failed because that %s.", err.Error())
}
}
}
}
// StopAll stop all send service in self.SenderServices normally which can delete the socket file.
func (self *SRpcService) StopAll() {
for _, service := range self.SendServices {
service.Close()
}
}
// Send call the corresponding rpc server.Send to send messager.
func (self *SRpcService) Send(contactType, contact, topic, msg, priority string) error {
self.lock.RLock()
sendService, ok := self.SendServices[contactType]
self.lock.RUnlock()
var err error
if !ok {
sendService, err = self.checkAndAddOne(contactType)
if err == ErrDial {
return ErrSendServiceNotFound
}
if err != nil {
return errors.Wrap(err, "Check or Add connection failed")
}
}
args := SSendArgs{
Contact: contact,
Topic: topic,
Message: msg,
Priority: priority,
}
reply := SSendReply{}
err = sendService.Call("Server.Send", &args, &reply)
if err != nil {
// should check and send again.
// Possible situation: notify always keep connection but remote guy have restarted
// so that connection valid.
sendService, err = self.checkAndAddOne(contactType)
if err != nil {
return errors.Wrap(err, "Check or Add connection failed")
}
err = sendService.Call("Server.Send", &args, &reply)
if err != nil {
return errors.Wrap(err, "Send message failed.")
}
if !reply.Success {
return errors.Error(fmt.Sprintf("Send message failed because that %s.", reply.Msg))
}
}
if !reply.Success {
if reply.Msg != NOTINIT {
return errors.Error(fmt.Sprintf("Send message failed because that %s.", reply.Msg))
}
// should check and send again
sendService, err = self.checkAndAddOne(contactType)
if err != nil {
return errors.Wrap(err, "Check or Add connection failed")
}
err = sendService.Call("Server.Send", &args, &reply)
if err != nil {
return errors.Wrap(err, "Send message failed.")
}
if !reply.Success {
return errors.Error(fmt.Sprintf("Send message failed because that %s.", reply.Msg))
}
}
return nil
}
// RestartService can restart remote rpc server and pass config info.
// When first init notify Server, must Call this function.
// When accept the request about changing config, must Call this function.
func (self *SRpcService) RestartService(config map[string]string, serviceName string) {
self.lock.RLock()
sendService, ok := self.SendServices[serviceName]
self.lock.RUnlock()
var err error
if !ok {
sendService, err = self.checkAndAddOne(serviceName)
if err != nil {
log.Debugf("Restart Failed: %s", err.Error())
return
}
}
args := SRestartArgs{Config: config}
reply := SSendReply{}
err = sendService.Call("Server.UpdateConfig", &args, &reply)
if err != nil || !reply.Success {
log.Errorf("Restart rpc serve whose name is %s failed.", serviceName)
return
}
}
// CheckAndAddOne check the status of service 'serviceName'
// If fail to dial to service, delete and remove sock file.
// if dial successfully, try to restart the service.
func (self *SRpcService) checkAndAddOne(serviceName string) (*rpc.Client, error) {
// Try to connect again
filename := filepath.Join(self.socketFileDir, serviceName+".sock")
rpcService, err := rpc.Dial("unix", filename)
if err != nil {
log.Debugf("Try to dial to service failed which unix socket file name is %s.", filename)
// This file maybe left behind inadvertently, so we should try to delete it
os.Remove(filename)
self.lock.Lock()
delete(self.SendServices, serviceName)
self.lock.Unlock()
return nil, ErrDial
}
// GetKeyValue to config rpc Service
config, err := ConfigManager.GetVauleByType(serviceName)
if err != nil {
log.Debugf("Init service error which unix socket file name is %s because that get config about this failed", filename)
return nil, ErrGetConfig
}
args := SRestartArgs{config}
reply := SSendReply{}
rpcService.Call("Server.UpdateConfig", &args, &reply)
if !reply.Success {
log.Debugf("Init service error which unix socket file name is %s because that %s.", filename, reply.Msg)
return nil, ErrUpdateConfig
}
self.lock.Lock()
self.SendServices[serviceName] = rpcService
self.lock.Unlock()
return rpcService, nil
}
func (self *SRpcService) updateService() error {
files, err := ioutil.ReadDir(self.socketFileDir)
if err != nil {
return errors.Wrapf(err, "read dir %s failed", self.socketFileDir)
}
original := make(map[string]*rpc.Client)
self.lock.RLock()
for serviceName, client := range self.SendServices {
original[serviceName] = client
}
self.lock.RUnlock()
for _, file := range files {
filename := file.Name()
if !file.IsDir() && strings.Contains(filename, ".sock") {
serviceName := filename[:len(filename)-5]
if _, ok := self.SendServices[serviceName]; ok {
delete(original, serviceName)
continue
}
self.checkAndAddOne(serviceName)
}
}
self.lock.Lock()
for serviceName := range original {
delete(self.SendServices, serviceName)
}
self.lock.Unlock()
for _, client := range original {
client.Close()
}
return nil
}
type SSendArgs struct {
Contact string
Topic string
Message string
Priority string
}
type SRestartArgs struct {
Config map[string]string
}
type SSendReply struct {
Success bool
Msg string
}

View File

@@ -0,0 +1,103 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package models
import (
"database/sql"
"yunion.io/x/pkg/util/stringutils"
"yunion.io/x/sqlchemy"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
)
type UUIDGenerator func() string
var (
DefaultUUIDGenerator = stringutils.UUID4
)
type SStandaloneResourceBase struct {
SResourceBase
ID string `width:"128" charset:"ascii" primary:"true" list:"user" create:"optional"`
}
func (model *SStandaloneResourceBase) BeforeInsert() {
if len(model.ID) == 0 {
model.ID = DefaultUUIDGenerator()
}
}
type SStandaloneResourceBaseManager struct {
SResourceBaseManager
}
func NewStandaloneResourceBaseManager(dt interface{}, tableName string, keyword string, keywordPlural string) SStandaloneResourceBaseManager {
return SStandaloneResourceBaseManager{NewResourceBaseManager(dt, tableName, keyword, keywordPlural)}
}
func (manager *SStandaloneResourceBaseManager) GetIStandaloneModelManager() db.IStandaloneModelManager {
return manager.GetVirtualObject().(db.IStandaloneModelManager)
}
func (manager *SStandaloneResourceBaseManager) FilterById(q *sqlchemy.SQuery, idStr string) *sqlchemy.SQuery {
return q.Equals("id", idStr)
}
func (manager *SStandaloneResourceBaseManager) FilterByNotId(q *sqlchemy.SQuery, idStr string) *sqlchemy.SQuery {
return q.NotEquals("id", idStr)
}
func (manager *SStandaloneResourceBaseManager) FetchById(idStr string) (db.IModel, error) {
return FetchById(manager.GetIStandaloneModelManager(), idStr)
}
func FetchById(manager db.IModelManager, idStr string) (db.IModel, error) {
q := manager.Query()
q = manager.FilterById(q, idStr)
count, err := q.CountWithError()
if err != nil {
return nil, err
}
if count == 1 {
obj, err := db.NewModelObject(manager)
if err != nil {
return nil, err
}
err = q.First(obj)
if err != nil {
return nil, err
} else {
return obj, nil
}
} else if count > 1 {
return nil, sqlchemy.ErrDuplicateEntry
} else {
return nil, sql.ErrNoRows
}
}
func (model *SStandaloneResourceBase) StandaloneModelManager() db.IStandaloneModelManager {
return model.GetModelManager().(db.IStandaloneModelManager)
}
func (model *SStandaloneResourceBase) GetId() string {
return model.ID
}
func (model *SStandaloneResourceBase) GetIStandaloneModel() db.IStandaloneModel {
return model.GetVirtualObject().(db.IStandaloneModel)
}

View File

@@ -0,0 +1,91 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package models
import (
"context"
"fmt"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/utils"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/util/rbacutils"
)
type SStatusStandaloneResourceBase struct {
SStandaloneResourceBase
Status string `width:"36" charset:"ascii" nullable:"false" default:"init" list:"user" create:"optional" update:"user"`
}
type SStatusStandaloneResourceBaseManager struct {
SStandaloneResourceBaseManager
}
func NewStatusStandaloneResourceBaseManager(dt interface{}, tableName string, keyword string, keywordPlural string) SStatusStandaloneResourceBaseManager {
return SStatusStandaloneResourceBaseManager{SStandaloneResourceBaseManager: NewStandaloneResourceBaseManager(dt, tableName, keyword, keywordPlural)}
}
func (model *SStatusStandaloneResourceBase) SetStatus(userCred mcclient.TokenCredential, status string, reason string) error {
if model.Status == status {
return nil
}
oldStatus := model.Status
_, err := db.Update(model, func() error {
model.Status = status
return nil
})
if err != nil {
return err
}
if userCred != nil {
notes := fmt.Sprintf("%s=>%s", oldStatus, status)
if len(reason) > 0 {
notes = fmt.Sprintf("%s: %s", notes, reason)
}
db.OpsLog.LogEvent(model, db.ACT_UPDATE_STATUS, notes, userCred)
}
return nil
}
func (model *SStatusStandaloneResourceBase) AllowPerformStatus(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool {
return db.IsAllowPerform(rbacutils.ScopeSystem, userCred, model, "status")
}
func (model *SStatusStandaloneResourceBase) PerformStatus(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) {
status, err := data.GetString("status")
if err != nil {
return nil, err
}
reason, _ := data.GetString("reason")
err = model.SetStatus(userCred, status, reason)
return nil, err
}
func (model *SStatusStandaloneResourceBase) IsInStatus(status ...string) bool {
return utils.IsInStringArray(model.Status, status)
}
func (model *SStatusStandaloneResourceBase) AllowGetDetailsStatus(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) bool {
return db.IsAllowGetSpec(rbacutils.ScopeSystem, userCred, model, "status")
}
func (model *SStatusStandaloneResourceBase) GetDetailsStatus(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (jsonutils.JSONObject, error) {
ret := jsonutils.NewDict()
ret.Add(jsonutils.NewString(model.Status), "status")
return ret, nil
}

View File

@@ -0,0 +1,32 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package options
import (
"yunion.io/x/onecloud/pkg/cloudcommon/options"
)
type NotifyOption struct {
options.CommonOptions
options.DBOptions
DingtalkEnabled bool `help:"Enable dingtalk"`
SocketFileDir string `help:"Socket file directory" default:"/etc/yunion/notify"`
UpdateInterval int `help:"Update send services interval(unit:s)" default:30`
VerifyEmailUrl string
ReSendScope int `help:"Resend all messages that have not been sent successfully within ReSendScope minutes"`
}
var Options NotifyOption

66
pkg/notify/service.go Normal file
View File

@@ -0,0 +1,66 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notify
import (
"os"
_ "github.com/go-sql-driver/mysql"
"yunion.io/x/log"
"yunion.io/x/onecloud/pkg/cloudcommon"
"yunion.io/x/onecloud/pkg/cloudcommon/app"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
common_options "yunion.io/x/onecloud/pkg/cloudcommon/options"
"yunion.io/x/onecloud/pkg/notify/models"
"yunion.io/x/onecloud/pkg/notify/options"
"yunion.io/x/onecloud/pkg/notify/utils"
)
func StartService() {
// parse options
opts := &options.Options
commonOpts := &options.Options.CommonOptions
dbOpts := &options.Options.DBOptions
baseOpts := &options.Options.BaseOptions
common_options.ParseOptions(opts, os.Args, "notify.conf", "notify")
// init auth
app.InitAuth(commonOpts, func() {
log.Infof("Auth complete!")
})
// Session for user manager in keystone
utils.InitSession(commonOpts)
// init handler
applicaion := app.InitApp(baseOpts, true)
InitHandlers(applicaion)
// init database
db.EnsureAppInitSyncDB(applicaion, dbOpts, models.InitDB)
defer cloudcommon.CloseDB()
// init rpc service
models.RpcService = models.NewSRpcService(opts.SocketFileDir)
models.RpcService.InitAll()
defer models.RpcService.StopAll()
go models.RpcService.UpdateServices(opts.UpdateInterval)
// start ReSend service
go models.ReSend(opts.ReSendScope)
app.ServeForever(applicaion, baseOpts)
}

View File

@@ -0,0 +1,59 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package utils
import (
"context"
"yunion.io/x/jsonutils"
"yunion.io/x/onecloud/pkg/cloudcommon/options"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/auth"
"yunion.io/x/onecloud/pkg/mcclient/modules"
)
var (
session *mcclient.ClientSession
)
func InitSession(options *options.CommonOptions) {
session = auth.GetAdminSession(context.Background(), options.Region, "v3")
}
func GetUserByID(id string) (jsonutils.JSONObject, error) {
return modules.UsersV3.Get(session, id, jsonutils.NewDict())
}
func GetUsersByGroupID(gid string) ([]string, error) {
ret, err := modules.Groups.GetUsers(session, gid)
if err != nil {
return nil, err
}
ids := make([]string, len(ret.Data))
for i := range ret.Data {
ids[i], _ = ret.Data[i].GetString("id")
}
return ids, nil
}
func GetUsernameByID(id string) (string, error) {
user, err := GetUserByID(id)
if err != nil {
return "", err
}
name, _ := user.GetString("name")
return name, nil
}

View File

@@ -0,0 +1,60 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package utils
import (
"bytes"
"context"
"fmt"
"math/rand"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/util/rbacutils"
)
func FetchOwnerId(ctx context.Context, manager db.IModelManager, userCred mcclient.TokenCredential, data jsonutils.JSONObject) (mcclient.IIdentityProvider, error) {
var ownerId mcclient.IIdentityProvider
var err error
if manager.ResourceScope() != rbacutils.ScopeSystem {
ownerId, err = manager.FetchOwnerId(ctx, data)
if err != nil {
return nil, httperrors.NewGeneralError(err)
}
}
if ownerId == nil {
ownerId = userCred
}
return ownerId, nil
}
func GenerateMobileToken() string {
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
token := fmt.Sprintf("%06v", rnd.Int31n(1000000))
return token
}
func GenerateEmailToken(tokenLen int) string {
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
token := new(bytes.Buffer)
for token.Len() < tokenLen {
token.WriteString(fmt.Sprintf("%x", rnd.Int31()))
}
return token.String()[:tokenLen]
}

View File

@@ -0,0 +1,33 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package utils
import (
"testing"
)
func TestGenEmailToken(t *testing.T) {
s := GenerateEmailToken(32)
if len(s) != 32 {
t.Error("email token length should be 32")
}
}
func TestGenMobileToke(t *testing.T) {
s := GenerateMobileToken()
if len(s) != 6 {
t.Errorf("mobile token length should be 6")
}
}