Merge pull request #15577 from gouqi11/notifyFix

fix(notify): refactor notify
This commit is contained in:
Zexi Li
2023-02-27 14:37:34 +08:00
committed by GitHub
96 changed files with 5606 additions and 4303 deletions
+129 -9
View File
@@ -15,14 +15,16 @@
package notify
import (
"reflect"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/gotypes"
"yunion.io/x/onecloud/pkg/apis"
)
type ConfigCreateInput struct {
apis.StandaloneResourceCreateInput
apis.DomainizedResourceInput
apis.DomainLevelResourceCreateInput
// description: config type
// required: true
@@ -32,7 +34,7 @@ type ConfigCreateInput struct {
// description: config content
// required: true
// example: {"app_id": "123456", "app_secret": "feishu_nihao"}
Content jsonutils.JSONObject `json:"content"`
Content *SNotifyConfigContent `json:"content"`
// description: attribution
// required: true
@@ -45,19 +47,17 @@ type ConfigUpdateInput struct {
// description: config content
// required: true
// example: {"app_id": "123456", "app_secret": "feishu_nihao"}
Content jsonutils.JSONObject `json:"content"`
Content *SNotifyConfigContent `json:"content"`
}
type ConfigDetails struct {
apis.StandaloneResourceDetails
apis.DomainizedResourceInfo
apis.DomainLevelResourceDetails
SConfig
}
type ConfigListInput struct {
apis.StandaloneResourceListInput
apis.DomainizedResourceListInput
apis.DomainLevelResourceListInput
Type string `json:"type"`
Attribution string `json:"attribution"`
}
@@ -71,7 +71,7 @@ type ConfigValidateInput struct {
// description: config content
// required: true
// example: {"app_id": "123456", "app_secret": "feishu_nihao"}
Content jsonutils.JSONObject `json:"content"`
Content *SNotifyConfigContent `json:"content"`
}
type ConfigValidateOutput struct {
@@ -95,3 +95,123 @@ type ConfigManagerGetTypesInput struct {
type ConfigManagerGetTypesOutput struct {
Types []string `json:"types"`
}
type SsNotification struct {
ContactType string
Topic string
Message string
Event SNotifyEvent
AdvanceDays int
}
type SBatchSendParams struct {
ContactType string
Contacts []string
Topic string
Message string
Priority string
Lang string
}
type SNotifyReceiver struct {
Contact string
DomainId string
Enabled bool
Lang string
callback func(error)
}
func (self *SNotifyReceiver) Callback(err error) {
if self.callback != nil && err != nil {
self.callback(err)
}
}
type SSendParams struct {
ContactType string
Contact string
Topic string
Message string
Priority string
Title string
RemoteTemplate string
Lang string
Receiver SNotifyReceiver
}
type SendParams struct {
Title string
Message string
Priority string
RemoteTemplate string
Topic string
Event string
Receivers SNotifyReceiver
EmailMsg *SEmailMessage
}
type SSMSSendParams struct {
AppKey string
AppSecret string
From string
To string
TemplateId string
TemplateParas string
Signature string
}
type NotifyConfig struct {
SNotifyConfigContent
Attribution string
DomainId string
}
func (self *NotifyConfig) GetDomainId() string {
if self.Attribution == CONFIG_ATTRIBUTION_SYSTEM {
return CONFIG_ATTRIBUTION_SYSTEM
}
return self.DomainId
}
type SNotifyConfigContent struct {
// Email
Hostname string
Hostport int
Password string
SslGlobal bool
Username string
SenderAddress string
//Lark
AppId string
AppSecret string
AccessToken string
// workwx
AgentId string
CorpId string
Secret string
// dingtalk
//AgentId string
//AppSecret string
AppKey string
// sms
AccessKeyId string
AccessKeySecret string
ServiceUrl string
Signature string
SmsDriver string
}
func (self SNotifyConfigContent) String() string {
return jsonutils.Marshal(self).String()
}
func (self SNotifyConfigContent) IsZero() bool {
return jsonutils.Marshal(self).Equals(jsonutils.Marshal(SNotifyConfigContent{}))
}
func init() {
gotypes.RegisterSerializable(reflect.TypeOf(&SNotifyConfigContent{}), func() gotypes.ISerializable {
return &SNotifyConfigContent{}
})
}
+12 -1
View File
@@ -14,7 +14,11 @@
package notify
import "yunion.io/x/onecloud/pkg/apis"
import (
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/apis"
)
const (
SERVICE_TYPE = apis.SERVICE_TYPE_NOTIFY
@@ -30,6 +34,8 @@ const (
DINGTALK_ROBOT = "dingtalk-robot"
WORKWX_ROBOT = "workwx-robot"
WEBHOOK = "webhook"
WEBHOOK_ROBOT = "webhook-robot"
WEBSOCKET = "websocket"
ROBOT = "robot"
@@ -139,3 +145,8 @@ const (
SUBSCRIBER_SCOPE_DOMAIN = "domain"
SUBSCRIBER_SCOPE_PROJECT = "project"
)
var (
ErrNoSuchMobile = errors.Error("no such mobile")
ErrIncompleteConfig = errors.Error("incomplete config")
)
+4 -4
View File
@@ -40,9 +40,10 @@ var (
ActionCreateBackupServer SAction = "add_backup_server"
ActionDelBackupServer SAction = "delete_backup_server"
ActionSyncCreate SAction = "sync_create"
ActionSyncUpdate SAction = "sync_update"
ActionSyncDelete SAction = "sync_delete"
ActionSyncCreate SAction = "sync_create"
ActionSyncUpdate SAction = "sync_update"
ActionSyncDelete SAction = "sync_delete"
ActionSyncAccountStatus SAction = "sync_account_status"
ActionOffline SAction = "offline"
ActionSystemPanic SAction = "panic"
@@ -53,7 +54,6 @@ var (
ActionLock SAction = "lock"
ActionExceedCount SAction = "exceed_count"
ActionSyncAccountStatus SAction = "sync_account_status"
ActionPasswordExpireSoon SAction = "password_expire_soon"
ActionWorkerBlock SAction = "woker_block"
ActionNetOutOfSync SAction = "net_out_of_sync"
+10 -4
View File
@@ -54,9 +54,8 @@ type NotificationCreateInput struct {
// description: notification tag
// required: false
// example: alert
Tag string `json:"tag"`
Metadata map[string]interface{} `json:"metadata"`
IgnoreNonexistentReceiver bool `json:"ignore_nonexistent_receiver"`
Tag string `json:"tag"`
IgnoreNonexistentReceiver bool `json:"ignore_nonexistent_receiver"`
}
type ReceiveDetail struct {
@@ -118,7 +117,13 @@ type NotificationManagerEventNotifyInput struct {
// description: event trigger sending notification
// required: true
// example: SERVER_DELETE
Event string `json:"event"`
Event string
ResourceType string
CloudAccountName string
Action SAction
// failed,succeed
Result SResult
// description: day left before the event
// required: false
// example: 0
@@ -131,6 +136,7 @@ type NotificationManagerEventNotifyInput struct {
// required: false
// example: f627e09f038645f08ce6880c8d9cb8fd
ProjectId string `json:"project_id"`
IsFailed SResult
}
type NotificationManagerEventNotifyOutput struct {
+4 -9
View File
@@ -22,9 +22,7 @@ import (
)
type ReceiverCreateInput struct {
apis.StatusStandaloneResourceCreateInput
apis.DomainizedResourceCreateInput
apis.EnabledBaseResourceCreateInput
apis.EnabledStatusDomainLevelResourceCreateInput
// description: user id in keystone
// example: adfb720ccdd34c638346ea4fa7a713a8
@@ -84,8 +82,7 @@ func (im SInternationalMobile) String() string {
}
type ReceiverDetails struct {
apis.StatusStandaloneResourceDetails
apis.DomainizedResourceInfo
apis.EnabledStatusDomainLevelResourceDetails
SReceiver
InternationalMobile SInternationalMobile `json:"international_mobile"`
@@ -105,9 +102,7 @@ type VerifiedInfo struct {
}
type ReceiverListInput struct {
apis.StatusStandaloneResourceListInput
apis.DomainizedResourceListInput
apis.EnabledResourceBaseListInput
apis.EnabledStatusDomainLevelResourceListInput
UID string `json:"uid"`
@@ -121,7 +116,7 @@ type ReceiverListInput struct {
}
type ReceiverUpdateInput struct {
apis.StatusStandaloneResourceBaseUpdateInput
apis.EnabledStatusDomainLevelResourceBaseUpdateInput
// description: user email
// example: example@gmail.com
+1
View File
@@ -18,6 +18,7 @@ import "yunion.io/x/onecloud/pkg/apis"
type RobotCreateInput struct {
apis.SharableVirtualResourceCreateInput
apis.EnabledBaseResourceCreateInput
// description: robot type
// enum: feishu,dingtalk,workwx,webhook
// example: webhook
+5
View File
@@ -0,0 +1,5 @@
package websocket
const (
SERVICE_TYPE_WEBSOCKET = "websocket"
)
@@ -12,4 +12,4 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package rpc // import "yunion.io/x/onecloud/pkg/notify/rpc"
package websocket // import "yunion.io/x/onecloud/pkg/apis"
+1
View File
@@ -31,6 +31,7 @@ type SKeystoneCacheObject struct {
Domain string `width:"128" charset:"utf8" nullable:"true"`
LastCheck time.Time `nullable:"true"`
Lang string `width:"8" charset:"ascii" nullable:"true" list:"domain" update:"domain" create:"domain_optional"`
}
func NewKeystoneCacheObjectManager(dt interface{}, tableName string, keyword string, keywordPlural string) SKeystoneCacheObjectManager {
+8 -3
View File
@@ -64,7 +64,7 @@ func init() {
func (manager *SUserCacheManager) updateUserCache(ctx context.Context, userCred mcclient.TokenCredential) {
manager.Save(ctx, userCred.GetUserId(), userCred.GetUserName(),
userCred.GetDomainId(), userCred.GetDomainName())
userCred.GetDomainId(), userCred.GetDomainName(), "")
}
func (manager *SUserCacheManager) FetchUserByIdOrName(ctx context.Context, idStr string) (*SUser, error) {
@@ -143,10 +143,11 @@ func (manager *SUserCacheManager) FetchUserFromKeystone(ctx context.Context, idS
name, _ := user.GetString("name")
domainId, _ := user.GetString("domain_id")
domainNmae, _ := user.GetString("project_domain")
return manager.Save(ctx, id, name, domainId, domainNmae)
lang, _ := user.GetString("lang")
return manager.Save(ctx, id, name, domainId, domainNmae, lang)
}
func (manager *SUserCacheManager) Save(ctx context.Context, idStr string, name string, domainId string, domain string) (*SUser, error) {
func (manager *SUserCacheManager) Save(ctx context.Context, idStr string, name string, domainId string, domain, lang string) (*SUser, error) {
lockman.LockRawObject(ctx, manager.KeywordPlural(), idStr)
defer lockman.ReleaseRawObject(ctx, manager.KeywordPlural(), idStr)
@@ -163,6 +164,9 @@ func (manager *SUserCacheManager) Save(ctx context.Context, idStr string, name s
obj.Domain = domain
obj.DomainId = domainId
obj.LastCheck = time.Now().UTC()
if len(lang) > 0 {
obj.Lang = lang
}
return nil
})
if err != nil {
@@ -178,6 +182,7 @@ func (manager *SUserCacheManager) Save(ctx context.Context, idStr string, name s
obj.Domain = domain
obj.DomainId = domainId
obj.LastCheck = time.Now().UTC()
obj.Lang = lang
err = manager.TableSpec().InsertOrUpdate(ctx, obj)
if err != nil {
return nil, err
+4 -3
View File
@@ -63,9 +63,10 @@ var (
ActionPendingDelete = api.ActionPendingDelete
ActionSyncCreate = api.ActionSyncCreate
ActionSyncUpdate = api.ActionSyncUpdate
ActionSyncDelete = api.ActionSyncDelete
ActionSyncCreate = api.ActionSyncCreate
ActionSyncUpdate = api.ActionSyncUpdate
ActionSyncDelete = api.ActionSyncDelete
ActionSyncAccountStatus = api.ActionSyncAccountStatus
)
type SEvent struct {
+4
View File
@@ -313,6 +313,8 @@ func EventNotify(ctx context.Context, userCred mcclient.TokenCredential, ep SEve
Priority: string(npk.NotifyPriorityNormal),
ProjectId: projectId,
ProjectDomainId: projectDomainId,
ResourceType: ep.ResourceType,
Action: ep.Action,
}
t := eventTask{
params: params,
@@ -336,6 +338,8 @@ func EventNotifyServiceAbnormal(ctx context.Context, userCred mcclient.TokenCred
Event: event.String(),
AdvanceDays: 0,
Priority: string(npk.NotifyPriorityNormal),
ResourceType: api.TOPIC_RESOURCE_SERVICE,
Action: api.ActionServiceAbnormal,
}
t := eventTask{
params: params,
-1
View File
@@ -215,7 +215,6 @@ func (manager *SGuestManager) ListItemFilter(
query api.ServerListInput,
) (*sqlchemy.SQuery, error) {
var err error
q, err = manager.SHostResourceBaseManager.ListItemFilter(ctx, q, userCred, query.HostFilterListInput)
if err != nil {
return nil, errors.Wrap(err, "SHostResourceBaseManager.ListItemFilter")
+1
View File
@@ -0,0 +1 @@
package websocket // import "yunion.io/x/cloudpods/pkg/mcclient/modules/websocket"
@@ -0,0 +1,17 @@
package websocket
import (
apis "yunion.io/x/onecloud/pkg/apis/websocket"
"yunion.io/x/onecloud/pkg/mcclient/modulebase"
)
/*
添加新manager注意事项:
1. version字段 -- 在endpoint中注册的url如果携带版本。例如http://x.x.x.x/api/v1,那么必须标注对应version字段。否者可能导致yunionapi报资源not found的错误。
*/
func newWebsocketManager(keyword, keywordPlural string, columns, adminColumns []string) modulebase.ResourceManager {
return modulebase.ResourceManager{
BaseManager: *modulebase.NewBaseManager(apis.SERVICE_TYPE_WEBSOCKET, "", "", columns, adminColumns),
Keyword: keyword, KeywordPlural: keywordPlural}
}
@@ -12,4 +12,27 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package oldmodels // import "yunion.io/x/onecloud/pkg/notify/oldmodels"
package websocket
import (
"yunion.io/x/onecloud/pkg/mcclient/modulebase"
)
type SWebsocketManager struct {
modulebase.ResourceManager
}
var (
Websockets SWebsocketManager
)
func init() {
Websockets = SWebsocketManager{newWebsocketManager("websocket", "websockets",
// user view
[]string{},
[]string{}, // admin view
)}
register(&Websockets)
}
+7
View File
@@ -0,0 +1,7 @@
package websocket
import "yunion.io/x/onecloud/pkg/mcclient/modulebase"
func register(mod modulebase.IBaseManager) {
modulebase.Register(mod)
}
-85
View File
@@ -1,85 +0,0 @@
// 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"
"yunion.io/x/pkg/errors"
notify_apis "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/notify/rpc/apis"
)
type INotifyService interface {
InitAll() error
StopAll()
UpdateServices(ctx context.Context, userCred mcclient.TokenCredential, isStart bool)
UpdateConfig(ctx context.Context, service string, config SConfig) error
Send(ctx context.Context, contactType string, args apis.SendParams) error
ContactByMobile(ctx context.Context, mobile, serviceName, domainId string) (string, error)
BatchSend(ctx context.Context, contactType string, args apis.BatchSendParams) ([]*apis.FailedRecord, error)
SendRobotMessage(ctx context.Context, rType string, receivers []*apis.SReceiver, title string, message string) ([]*apis.FailedRecord, error)
AddConfig(ctx context.Context, service string, config SConfig) error
DeleteConfig(ctx context.Context, service, domainId string) error
ValidateConfig(ctx context.Context, cType string, configs map[string]string) (isValid bool, message string, err error)
}
type SSendParams struct {
ContactType string
Contact string
Topic string
Message string
Priority string
Lang string
}
type SBatchSendParams struct {
ContactType string
Contacts []string
Topic string
Message string
Priority string
Lang string
}
type IServiceConfigStore interface {
GetConfigs(service string) ([]SConfig, error)
SetConfig(service string, config SConfig) error
}
type SNotification struct {
ContactType string
Topic string
Message string
Event notify_apis.SNotifyEvent
AdvanceDays int
}
type ITemplateStore interface {
// NotifyFilter(contactType, topic, msg, lang string) (params apis.SendParams, err error)
FillWithTemplate(ctx context.Context, lang string, notification SNotification) (params apis.SendParams, err error)
}
type SConfig struct {
Config map[string]string
DomainId string
}
var (
ErrNoSuchMobile = errors.Error("no such mobile")
ErrIncompleteConfig = errors.Error("incomplete config")
)
+108 -352
View File
@@ -26,30 +26,28 @@ import (
"yunion.io/x/pkg/utils"
"yunion.io/x/sqlchemy"
"yunion.io/x/onecloud/pkg/apis/notify"
api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/cloudcommon/consts"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
"yunion.io/x/onecloud/pkg/cloudcommon/policy"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/auth"
notifyv2 "yunion.io/x/onecloud/pkg/notify"
"yunion.io/x/onecloud/pkg/notify/oldmodels"
"yunion.io/x/onecloud/pkg/notify/options"
"yunion.io/x/onecloud/pkg/util/logclient"
"yunion.io/x/onecloud/pkg/util/stringutils2"
)
type SConfigManager struct {
db.SStandaloneResourceBaseManager
db.SDomainizedResourceBaseManager
db.SDomainLevelResourceBaseManager
}
var ConfigManager *SConfigManager
var ConfigMap map[string]SConfig
func init() {
ConfigManager = &SConfigManager{
SStandaloneResourceBaseManager: db.NewStandaloneResourceBaseManager(
SDomainLevelResourceBaseManager: db.NewDomainLevelResourceBaseManager(
SConfig{},
"configs_tbl",
"notifyconfig",
@@ -60,63 +58,45 @@ func init() {
}
type SConfig struct {
db.SStandaloneResourceBase
db.SDomainizedResourceBase
db.SDomainLevelResourceBase
Type string `width:"15" nullable:"false" create:"required" get:"domain" list:"domain" index:"true"`
Content jsonutils.JSONObject `nullable:"false" create:"required" update:"domain" get:"domain" list:"domain"`
Attribution string `width:"8" nullable:"false" default:"system" get:"domain" list:"domain" create:"optional"`
Type string `width:"15" nullable:"false" create:"required" get:"domain" list:"domain" index:"true"`
Content *api.SNotifyConfigContent `nullable:"false" create:"required" update:"domain" get:"domain" list:"domain"`
Attribution string `width:"8" nullable:"false" default:"system" get:"domain" list:"domain" create:"optional"`
}
func (cm *SConfigManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input api.ConfigCreateInput) (api.ConfigCreateInput, error) {
var err error
input.StandaloneResourceCreateInput, err = cm.SStandaloneResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, input.StandaloneResourceCreateInput)
input.DomainLevelResourceCreateInput, err = cm.SDomainLevelResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, input.DomainLevelResourceCreateInput)
if err != nil {
return input, err
}
if len(input.ProjectDomainId) > 0 {
_, input.DomainizedResourceInput, err = db.ValidateDomainizedResourceInput(ctx, input.DomainizedResourceInput)
if err != nil {
return input, err
}
}
if !utils.IsInStringArray(input.Type, []string{api.EMAIL, api.MOBILE, api.DINGTALK, api.FEISHU, api.WEBCONSOLE, api.WORKWX}) {
return input, httperrors.NewInputParameterError("unkown type %q", input.Type)
if !utils.IsInStringArray(input.Type, GetSenderTypes()) {
return input, httperrors.NewInputParameterError("unkown type %s allow: %s", input.Type, GetSenderTypes())
}
if !utils.IsInStringArray(input.Attribution, []string{api.CONFIG_ATTRIBUTION_SYSTEM, api.CONFIG_ATTRIBUTION_DOMAIN}) {
return input, httperrors.NewInputParameterError("invalid attribution, need %q or %q", api.CONFIG_ATTRIBUTION_SYSTEM, api.CONFIG_ATTRIBUTION_DOMAIN)
}
if input.Attribution == api.CONFIG_ATTRIBUTION_SYSTEM {
allowScope, _ := policy.PolicyManager.AllowScope(userCred, consts.GetServiceType(), ConfigManager.KeywordPlural(), policy.PolicyActionCreate)
if allowScope != rbacscope.ScopeSystem {
return input, httperrors.NewInputParameterError("No permission to set %q attribution", api.CONFIG_ATTRIBUTION_SYSTEM)
}
}
if input.Content == nil {
return input, httperrors.NewMissingParameterError("content")
}
config, err := cm.Config(input.Type, input.ProjectDomainId, input.Attribution)
if err == nil && config != nil {
if config != nil {
return input, httperrors.NewDuplicateResourceError("duplicate type %q", input.Type)
}
if err != nil && errors.Cause(err) != sql.ErrNoRows {
return input, err
}
driver := GetDriver(input.Type)
// validate
configs := make(map[string]string)
err = input.Content.Unmarshal(&configs)
message, err := driver.ValidateConfig(api.NotifyConfig{
SNotifyConfigContent: *input.Content,
Attribution: input.Attribution,
DomainId: input.ProjectDomainId,
})
if err != nil {
return input, err
}
isValid, message, err := NotifyService.ValidateConfig(ctx, input.Type, configs)
if err != nil {
if errors.Cause(err) == errors.ErrNotImplemented {
return input, httperrors.NewNotImplementedError("validating config of %s", input.Type)
}
return input, err
}
if !isValid {
return input, httperrors.NewInputParameterError(message)
return input, errors.Wrapf(err, message)
}
if len(input.Name) == 0 {
input.Name = input.Type
@@ -124,73 +104,57 @@ func (cm *SConfigManager) ValidateCreateData(ctx context.Context, userCred mccli
return input, nil
}
func (c *SConfig) CustomizeCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) error {
err := c.SStandaloneResourceBase.CustomizeCreate(ctx, userCred, ownerId, query, data)
func (c *SConfig) PostCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) {
c.SDomainLevelResourceBase.PostCreate(ctx, userCred, ownerId, query, data)
err := c.StartRepullSubcontactTask(ctx, userCred, false)
if err != nil {
return err
log.Errorf("unable to StartRepullSubcontactTask: %v", err)
}
if c.Attribution == api.CONFIG_ATTRIBUTION_DOMAIN || c.Attribution == "" {
c.Attribution = api.CONFIG_ATTRIBUTION_DOMAIN
c.DomainId, _ = data.GetString("project_domain_id")
if c.DomainId == "" {
c.DomainId = userCred.GetProjectDomainId()
}
ConfigMap[c.Type] = *c
}
func (c *SConfig) GetNotifyConfig() api.NotifyConfig {
return api.NotifyConfig{
SNotifyConfigContent: *c.Content,
Attribution: c.Attribution,
DomainId: c.DomainId,
}
return nil
}
func (c *SConfig) ValidateUpdateData(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.ConfigUpdateInput) (api.ConfigUpdateInput, error) {
// validate
configs := make(map[string]string)
err := input.Content.Unmarshal(&configs)
q := ConfigManager.Query()
q = q.Equals("type", c.Type)
confs := []SConfig{}
err := db.FetchModelObjects(ConfigManager, q, &confs)
if err != nil {
return input, err
return input, errors.Wrapf(err, "config type:%s", c.Type)
}
if len(confs) == 0 {
return input, errors.Wrapf(errors.ErrNotFound, "config type:%s", c.Type)
}
// check if changed
if c.Content.Equals(input.Content) {
return input, nil
}
isValid, message, err := NotifyService.ValidateConfig(ctx, c.Type, configs)
if err != nil {
if errors.Cause(err) == errors.ErrNotImplemented {
return input, httperrors.NewNotImplementedError("validating config of %s", c.Type)
if input.Content != nil {
driver := GetDriver(c.Type)
message, err := driver.ValidateConfig(api.NotifyConfig{
DomainId: c.DomainId,
Attribution: c.Attribution,
SNotifyConfigContent: *input.Content,
})
if err != nil {
return input, errors.Wrapf(err, message)
}
return input, err
}
if !isValid {
return input, httperrors.NewInputParameterError(message)
}
return input, nil
}
func (c *SConfig) PostCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) {
c.SStandaloneResourceBase.PostCreate(ctx, userCred, ownerId, query, data)
configMap := make(map[string]string)
err := c.Content.Unmarshal(&configMap)
if err != nil {
log.Errorf("unable to unmarshal: %v", err)
return
}
NotifyService.AddConfig(ctx, c.Type, c.Config())
err = c.StartRepullSubcontactTask(ctx, userCred, false)
if err != nil {
log.Errorf("unable to StartRepullSubcontactTask: %v", err)
}
}
func (c *SConfig) PostUpdate(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) {
c.SStandaloneResourceBase.PostUpdate(ctx, userCred, query, data)
configMap := make(map[string]string)
err := c.Content.Unmarshal(&configMap)
if err != nil {
log.Errorf("unable to unmarshal: %v", err)
return
config := c.GetNotifyConfig()
ConfigMap[c.Type] = SConfig{
Content: &config.SNotifyConfigContent,
}
NotifyService.UpdateConfig(ctx, c.Type, notifyv2.SConfig{
Config: configMap,
DomainId: c.DomainId,
})
err = c.StartRepullSubcontactTask(ctx, userCred, false)
err := c.StartRepullSubcontactTask(ctx, userCred, false)
if err != nil {
log.Errorf("unable to StartRepullSubcontactTask: %v", err)
}
@@ -198,7 +162,7 @@ func (c *SConfig) PostUpdate(ctx context.Context, userCred mcclient.TokenCredent
func (c *SConfig) PreDelete(ctx context.Context, userCred mcclient.TokenCredential) {
c.SStandaloneResourceBase.PreDelete(ctx, userCred)
NotifyService.DeleteConfig(ctx, c.Type, c.Config().DomainId)
delete(ConfigMap, c.Type)
}
func (c *SConfig) CustomizeDelete(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) error {
@@ -206,12 +170,7 @@ func (c *SConfig) CustomizeDelete(ctx context.Context, userCred mcclient.TokenCr
if err != nil {
return err
}
NotifyService.DeleteConfig(ctx, c.Type, c.Config().DomainId)
err = c.StartRepullSubcontactTask(ctx, userCred, true)
if err != nil {
return errors.Wrap(err, "unable to start repull subcontact")
}
return err
return c.StartRepullSubcontactTask(ctx, userCred, true)
}
func (c *SConfig) Delete(ctx context.Context, userCred mcclient.TokenCredential) error {
@@ -219,7 +178,24 @@ func (c *SConfig) Delete(ctx context.Context, userCred mcclient.TokenCredential)
}
func (c *SConfig) RealDelete(ctx context.Context, userCred mcclient.TokenCredential) error {
return c.SStandaloneResourceBase.Delete(ctx, userCred)
delete(ConfigMap, c.Type)
return c.SDomainLevelResourceBase.Delete(ctx, userCred)
}
func (self *SConfig) GetReceivers() ([]SReceiver, error) {
subq := SubContactManager.Query("receiver_id").Equals("type", self.Type).SubQuery()
q := ReceiverManager.Query()
if self.Attribution == api.CONFIG_ATTRIBUTION_DOMAIN {
q = q.Equals("domain_id", self.DomainId)
} else {
// The system-level config update should not affect the receiver under the domain with config
configq := ConfigManager.Query("domain_id").Equals("type", self.Type).Equals("attribution", api.CONFIG_ATTRIBUTION_DOMAIN).SubQuery()
q = q.NotIn("domain_id", configq)
}
q = q.Join(subq, sqlchemy.Equals(q.Field("id"), subq.Field("receiver_id")))
ret := []SReceiver{}
err := db.FetchModelObjects(ReceiverManager, q, &ret)
return ret, err
}
func (c *SConfig) StartRepullSubcontactTask(ctx context.Context, userCred mcclient.TokenCredential, del bool) error {
@@ -296,11 +272,7 @@ func (cm *SConfigManager) allContactType() ([]string, error) {
}
func (self *SConfigManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, input api.ConfigListInput) (*sqlchemy.SQuery, error) {
q, err := self.SStandaloneResourceBaseManager.ListItemFilter(ctx, q, userCred, input.StandaloneResourceListInput)
if err != nil {
return nil, err
}
q, err = self.SDomainizedResourceBaseManager.ListItemFilter(ctx, q, userCred, input.DomainizedResourceListInput)
q, err := self.SDomainLevelResourceBaseManager.ListItemFilter(ctx, q, userCred, input.DomainLevelResourceListInput)
if err != nil {
return nil, err
}
@@ -311,12 +283,11 @@ func (self *SConfigManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQue
if len(input.Attribution) > 0 {
q = q.Equals("attribution", input.Attribution)
}
return q, nil
}
func (manager *SConfigManager) ListItemExportKeys(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, keys stringutils2.SSortedStrings) (*sqlchemy.SQuery, error) {
return manager.SStandaloneResourceBaseManager.ListItemExportKeys(ctx, q, userCred, keys)
return manager.SDomainLevelResourceBaseManager.ListItemExportKeys(ctx, q, userCred, keys)
}
func (cm *SConfigManager) FetchCustomizeColumns(
@@ -327,22 +298,16 @@ func (cm *SConfigManager) FetchCustomizeColumns(
fields stringutils2.SSortedStrings,
isList bool,
) []api.ConfigDetails {
sRows := cm.SStandaloneResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
dRows := cm.SDomainizedResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
sRows := cm.SDomainLevelResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
rows := make([]api.ConfigDetails, len(objs))
for i := range rows {
rows[i].StandaloneResourceDetails = sRows[i]
rows[i].DomainizedResourceInfo = dRows[i]
rows[i].DomainLevelResourceDetails = sRows[i]
}
return rows
}
func (cm *SConfigManager) QueryDistinctExtraField(q *sqlchemy.SQuery, field string) (*sqlchemy.SQuery, error) {
q, err := cm.SStandaloneResourceBaseManager.QueryDistinctExtraField(q, field)
if err != nil {
return q, nil
}
q, err = cm.SDomainizedResourceBaseManager.QueryDistinctExtraField(q, field)
q, err := cm.SDomainLevelResourceBaseManager.QueryDistinctExtraField(q, field)
if err != nil {
return q, nil
}
@@ -350,11 +315,7 @@ func (cm *SConfigManager) QueryDistinctExtraField(q *sqlchemy.SQuery, field stri
}
func (cm *SConfigManager) OrderByExtraFields(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, query api.ConfigListInput) (*sqlchemy.SQuery, error) {
q, err := cm.SStandaloneResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.StandaloneResourceListInput)
if err != nil {
return nil, err
}
q, err = cm.SDomainizedResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.DomainizedResourceListInput)
q, err := cm.SDomainLevelResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.DomainLevelResourceListInput)
if err != nil {
return nil, err
}
@@ -362,155 +323,49 @@ func (cm *SConfigManager) OrderByExtraFields(ctx context.Context, q *sqlchemy.SQ
}
func (cm *SConfigManager) PerformValidate(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.ConfigValidateInput) (api.ConfigValidateOutput, error) {
var (
output api.ConfigValidateOutput
err error
)
if !utils.IsInStringArray(input.Type, []string{api.EMAIL, api.MOBILE, api.DINGTALK, api.FEISHU, api.WEBCONSOLE, api.WORKWX, api.FEISHU_ROBOT, api.DINGTALK_ROBOT, api.WORKWX_ROBOT}) {
output := api.ConfigValidateOutput{}
if !utils.IsInStringArray(input.Type, GetSenderTypes()) {
return output, httperrors.NewInputParameterError("unkown type %q", input.Type)
}
if input.Content == nil {
return output, httperrors.NewMissingParameterError("content")
}
// validate
configs := make(map[string]string)
err = input.Content.Unmarshal(&configs)
driver := GetDriver(input.Type)
message, err := driver.ValidateConfig(api.NotifyConfig{
SNotifyConfigContent: *input.Content,
})
if err != nil {
return output, err
}
isValid, message, err := NotifyService.ValidateConfig(ctx, input.Type, configs)
if err != nil {
if errors.Cause(err) == errors.ErrNotImplemented {
return output, httperrors.NewNotImplementedError("validating config of %s", input.Type)
}
return output, err
}
if !isValid {
output.IsValid = false
output.Message = message
} else {
output.IsValid = true
return output, errors.Wrapf(err, message)
}
output.IsValid = true
output.Message = message
return output, nil
}
func (self *SConfigManager) InitializeData() error {
ctx := context.Background()
userCred := auth.AdminCredential()
// fetch all configs
configs := make([]oldmodels.SConfig, 0, 5)
q := oldmodels.ConfigManager.Query()
err := db.FetchModelObjects(oldmodels.ConfigManager, q, &configs)
func (confManager *SConfigManager) InitializeData() error {
q := confManager.Query()
res := []SConfig{}
err := db.FetchModelObjects(confManager, q, &res)
if err != nil {
return errors.Wrap(err, "db.FetchModelObjects")
return errors.Wrap(err, "init configMap err")
}
// build type==>config map
tcMap := make(map[string][]*oldmodels.SConfig)
for i := range configs {
t := configs[i].Type
if _, ok := tcMap[t]; !ok {
tcMap[t] = make([]*oldmodels.SConfig, 0, 3)
ConfigMap = make(map[string]SConfig)
for _, config := range res {
ConfigMap[config.Type] = config
driver := GetDriver(config.Type)
if config.Type == notify.EMAIL || config.Type == notify.MOBILE {
continue
}
tcMap[t] = append(tcMap[t], &configs[i])
}
for t, configs := range tcMap {
cMap := make(map[string]string)
if t == api.EMAIL {
for _, config := range configs {
switch config.KeyText {
case "mail.username":
cMap["username"] = config.ValueText
case "mail.password":
cMap["password"] = config.ValueText
case "mail.smtp.hostname":
cMap["hostname"] = config.ValueText
case "mail.smtp.hostport":
cMap["hostport"] = config.ValueText
}
}
} else {
for _, config := range configs {
cMap[config.KeyText] = config.ValueText
}
}
newConfig := SConfig{
Type: t,
Content: jsonutils.Marshal(cMap),
}
err := self.TableSpec().Insert(ctx, &newConfig)
err := driver.GetAccessToken()
if err != nil {
return errors.Wrap(err, "TableSpec().Insert")
}
for _, config := range configs {
err := config.Delete(ctx, userCred)
if err != nil {
return errors.Wrap(err, "Delete")
}
}
}
// init webconsole's config
q = self.Query().Equals("type", api.WEBCONSOLE)
wsConfigs := make([]SConfig, 0, 2)
err = db.FetchModelObjects(self, q, &wsConfigs)
if err != nil && errors.Cause(err) != sql.ErrNoRows {
return errors.Wrap(err, "db.FetchModelObjects")
}
if len(wsConfigs) > 1 {
for i := 1; i < len(wsConfigs); i++ {
err := wsConfigs[i].Delete(ctx, userCred)
if err != nil {
return errors.Wrap(err, "Delete redundant")
}
}
}
var config *SConfig
if len(wsConfigs) > 0 {
config = &wsConfigs[0]
} else {
config = &SConfig{
Type: api.WEBCONSOLE,
}
}
config.Content = jsonutils.Marshal(map[string]string{
"auth_uri": options.Options.AuthURL,
"admin_user": options.Options.AdminUser,
"admin_password": options.Options.AdminPassword,
"admin_tenant_name": options.Options.AdminProject,
})
err = self.TableSpec().InsertOrUpdate(context.TODO(), config)
// init config name
q = self.Query().IsNullOrEmpty("name")
enConfigs := make([]SConfig, 0)
err = db.FetchModelObjects(self, q, &enConfigs)
if err != nil {
return errors.Wrap(err, "unable to get configs with empty name")
}
for i := range enConfigs {
c := &enConfigs[i]
name, err := db.GenerateAlterName(c, enConfigs[i].Type)
if err != nil {
return errors.Wrap(err, "unable to generate alter name")
}
_, err = db.Update(c, func() error {
c.Name = name
return nil
})
if err != nil {
return errors.Wrap(err, "unable to update name")
session := auth.GetAdminSession(context.Background(), options.Options.Region)
logclient.AddSimpleActionLog(&config, logclient.ACT_INIT_NOTIFY_CONFIGMAP, err, session.GetToken(), false)
}
}
return nil
}
func (cm *SConfigManager) ResourceScope() rbacscope.TRbacScope {
return rbacscope.ScopeDomain
}
func (cm *SConfigManager) FilterByOwner(q *sqlchemy.SQuery, owner mcclient.IIdentityProvider, scope rbacscope.TRbacScope) *sqlchemy.SQuery {
switch scope {
case rbacscope.ScopeDomain, rbacscope.ScopeProject:
@@ -536,11 +391,9 @@ func (self *SConfigManager) Configs(contactType string) ([]SConfig, error) {
func (self *SConfigManager) Config(contactType, domainId string, attribution string) (*SConfig, error) {
q := self.Query()
q = q.Equals("type", contactType)
if attribution == api.CONFIG_ATTRIBUTION_SYSTEM {
q = q.Equals("attribution", api.CONFIG_ATTRIBUTION_SYSTEM)
} else {
q = q.Equals("domain_id", domainId).Equals("attribution", api.CONFIG_ATTRIBUTION_DOMAIN)
q = q.Equals("type", contactType).Equals("attribution", attribution)
if attribution == api.CONFIG_ATTRIBUTION_DOMAIN {
q = q.Equals("domain_id", domainId)
}
var config SConfig
err := q.First(&config)
@@ -551,107 +404,10 @@ func (self *SConfigManager) Config(contactType, domainId string, attribution str
}
func (self *SConfigManager) HasSystemConfig(contactType string) (bool, error) {
q := self.Query().Equals("type", contactType).Equals("attribution", "system")
q := self.Query().Equals("type", contactType).Equals("attribution", api.CONFIG_ATTRIBUTION_SYSTEM)
c, err := q.CountWithError()
if err != nil {
return false, err
}
return c > 0, nil
}
func (self *SConfigManager) BatchCheckConfig(contactType string, domainIds []string) ([]bool, error) {
domainIdSet := sets.NewString(domainIds...)
var configs = make([]SConfig, 0, 2)
q := self.Query().Equals("type", contactType).Equals("attribution", "domain").In("domain_id", domainIdSet.UnsortedList())
err := q.All(&configs)
if err != nil {
return nil, errors.Wrapf(err, "fail to fetch SConfigs by type %s", contactType)
}
for i := range configs {
if domainIdSet.Has(configs[i].DomainId) {
domainIdSet.Delete(configs[i].DomainId)
}
}
ret := make([]bool, len(domainIds))
for i := range domainIds {
if domainIdSet.Has(domainIds[i]) {
// no config of domainId, use default one
ret[i] = false
}
ret[i] = true
}
return ret, nil
}
func (self *SConfigManager) GetConfigs(contactType string) ([]notifyv2.SConfig, error) {
configs, err := self.Configs(contactType)
if err != nil {
return nil, err
}
ret := make([]notifyv2.SConfig, 0, len(configs))
for i := range configs {
ret = append(ret, configs[i].Config())
}
return ret, nil
}
func (manager *SConfigManager) getEmailConfig() (*api.SEmailConfig, error) {
confs, err := manager.GetConfigs(api.EMAIL)
if err != nil {
return nil, errors.Wrap(err, "GetConfigs")
}
if len(confs) == 0 {
return nil, errors.Wrap(errors.ErrNotSupported, "email not supported")
}
conf := api.SEmailConfig{}
err = jsonutils.Marshal(confs[0].Config).Unmarshal(&conf)
if err != nil {
return nil, errors.Wrap(err, "Unmarshal")
}
return &conf, nil
}
func (self *SConfigManager) SetConfig(contactType string, config notifyv2.SConfig) error {
content := jsonutils.Marshal(config.Config)
sConfig := &SConfig{
Type: contactType,
Content: content,
}
sConfig.DomainId = config.DomainId
if sConfig.DomainId == "" {
sConfig.Attribution = api.CONFIG_ATTRIBUTION_SYSTEM
} else {
sConfig.Attribution = api.CONFIG_ATTRIBUTION_DOMAIN
}
return self.TableSpec().InsertOrUpdate(context.Background(), sConfig)
}
func (self *SConfig) Config() notifyv2.SConfig {
c := make(map[string]string)
_ = self.Content.Unmarshal(&c)
sc := notifyv2.SConfig{
Config: c,
}
if self.Attribution == api.CONFIG_ATTRIBUTION_DOMAIN {
sc.DomainId = self.DomainId
}
return sc
}
func intersection(sa1, sa2 []string) []string {
set1 := sets.NewString(sa1...)
set2 := sets.NewString(sa2...)
return set1.Intersection(set2).UnsortedList()
}
func difference(sa1, sa2 []string) []string {
set1 := sets.NewString(sa1...)
set2 := sets.NewString(sa2...)
return set1.Difference(set2).UnsortedList()
}
func union(sa1, sa2 []string) []string {
set1 := sets.NewString(sa1...)
set2 := sets.NewString(sa2...)
return set1.Union(set2).UnsortedList()
}
+5 -11
View File
@@ -31,7 +31,6 @@ import (
"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/sender"
"yunion.io/x/onecloud/pkg/util/stringutils2"
)
@@ -134,7 +133,7 @@ func (eq *SEmailQueue) PostCreate(
}
func (eq *SEmailQueue) doSendAsync() {
sender.Worker.Run(eq, nil, nil)
Worker.Run(eq, nil, nil)
}
func (eq *SEmailQueue) Dump() string {
@@ -147,26 +146,21 @@ func (eq *SEmailQueue) Run() {
}
func (eq *SEmailQueue) doSend(ctx context.Context) {
conf, err := ConfigManager.getEmailConfig()
if err != nil {
eq.setStatus(ctx, api.EmailFail, err)
return
}
log.Debugf("conf: %s", jsonutils.Marshal(conf))
msg, err := eq.getMessage()
if err != nil {
eq.setStatus(ctx, api.EmailFail, err)
return
}
log.Debugf("msg: %s", jsonutils.Marshal(msg))
eq.setStatus(ctx, api.EmailSending, nil)
err = sender.SendEmail(conf, msg)
driver := GetDriver(api.EMAIL)
driver.Send(api.SendParams{
EmailMsg: msg,
})
if err != nil {
eq.setStatus(ctx, api.EmailFail, err)
return
}
eq.setStatus(ctx, api.EmailSuccess, nil)
return
}
func (eq *SEmailQueue) getMessage() (*api.SEmailMessage, error) {
+13 -9
View File
@@ -41,18 +41,22 @@ type SEvent struct {
// 资源创建时间
CreatedAt time.Time `nullable:"false" created_at:"true" index:"true" get:"user" list:"user" json:"created_at"`
Message string
Event string `width:"64" nullable:"true"`
AdvanceDays int
TopicId string `width:"128" nullable:"true" index:"true"`
Message string
Event string `width:"64" nullable:"true"`
ResourceType string `width:"64" nullable:"true"`
Action string `width:"64" nullable:"true"`
AdvanceDays int
TopicId string `width:"128" nullable:"true" index:"true"`
}
func (e *SEventManager) CreateEvent(ctx context.Context, event, topicId, message string, advanceDays int) (*SEvent, error) {
func (e *SEventManager) CreateEvent(ctx context.Context, event, topicId, message, action, resourceType string, advanceDays int) (*SEvent, error) {
eve := &SEvent{
Message: message,
Event: event,
AdvanceDays: advanceDays,
TopicId: topicId,
Message: message,
Event: event,
AdvanceDays: advanceDays,
TopicId: topicId,
Action: action,
ResourceType: resourceType,
}
err := e.TableSpec().Insert(ctx, eve)
if err != nil {
+9 -7
View File
@@ -36,8 +36,6 @@ import (
api "yunion.io/x/onecloud/pkg/apis/notify"
schapi "yunion.io/x/onecloud/pkg/apis/scheduledtask"
"yunion.io/x/onecloud/pkg/i18n"
notifyv2 "yunion.io/x/onecloud/pkg/notify"
rpcapi "yunion.io/x/onecloud/pkg/notify/rpc/apis"
)
type SEventDisplay struct {
@@ -94,8 +92,9 @@ func (lt *SLocalTemplateManager) detailsDisplay(resourceType string, details *js
}
}
func (lt *SLocalTemplateManager) FillWithTemplate(ctx context.Context, lang string, no notifyv2.SNotification) (params rpcapi.SendParams, err error) {
out, event := rpcapi.SendParams{}, no.Event
func (lt *SLocalTemplateManager) FillWithTemplate(ctx context.Context, lang string, no api.SsNotification) (params api.SendParams, err error) {
// return api.SendParams{}, nil
out, event := api.SendParams{}, no.Event
rtStr, aStr, resultStr := event.ResourceType(), string(event.Action()), string(event.Result())
msgObj, err := jsonutils.ParseString(no.Message)
if err != nil {
@@ -111,7 +110,7 @@ func (lt *SLocalTemplateManager) FillWithTemplate(ctx context.Context, lang stri
webhookMsg.Set("result", jsonutils.NewString(resultStr))
webhookMsg.Set("resource_details", msg)
if no.ContactType == api.WEBHOOK {
return rpcapi.SendParams{
return api.SendParams{
Title: no.Event.StringWithDeli("_"),
Message: webhookMsg.String(),
}, nil
@@ -152,7 +151,6 @@ func (lt *SLocalTemplateManager) FillWithTemplate(ctx context.Context, lang stri
return out, err
}
}
// get content
content, err := lt.fillWithTemplate(ctx, "content", no.ContactType, lang, event, templateParams)
if err != nil {
@@ -259,7 +257,6 @@ func init() {
func (lt *SLocalTemplateManager) getTemplate(ctx context.Context, titleOrContent string, contactType string, topic string, lang string) (*template.Template, error) {
key := fmt.Sprintf("%s.%s@%s", topic, titleOrContent, lang)
obj, ok := lt.templatesTable.Load(key)
var elem sTemplateElem
if !ok {
@@ -527,6 +524,11 @@ func init() {
"created",
"创建",
},
sI18nElme{
string(api.ActionUpdate),
"update",
"更新",
},
sI18nElme{
string(api.ActionDelete),
"deleted",
+176 -302
View File
@@ -18,6 +18,7 @@ import (
"context"
"database/sql"
"fmt"
"strings"
"time"
"yunion.io/x/jsonutils"
@@ -31,11 +32,9 @@ import (
api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
"yunion.io/x/onecloud/pkg/cloudcommon/validators"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/image/policy"
"yunion.io/x/onecloud/pkg/mcclient"
notifyv2 "yunion.io/x/onecloud/pkg/notify"
"yunion.io/x/onecloud/pkg/notify/oldmodels"
"yunion.io/x/onecloud/pkg/notify/options"
"yunion.io/x/onecloud/pkg/util/stringutils2"
)
@@ -45,7 +44,6 @@ type SNotificationManager struct {
}
var NotificationManager *SNotificationManager
var NotifyService notifyv2.INotifyService
func init() {
NotificationManager = &SNotificationManager{
@@ -60,22 +58,22 @@ func init() {
NotificationManager.TableSpec().AddIndex(false, "deleted", "contact_type", "topic_type")
}
// 站内信
type SNotification struct {
db.SStatusStandaloneResourceBase
ContactType string `width:"16" nullable:"false" create:"required" list:"user" get:"user"`
ContactType string `width:"128" nullable:"true" create:"optional" list:"user" get:"user"`
// swagger:ignore
Topic string `width:"128" nullable:"true" create:"required" search:"user"`
Topic string `width:"128" nullable:"true" create:"required" list:"user" get:"user"`
Priority string `width:"16" nullable:"true" create:"optional" list:"user" get:"user"`
// swagger:ignore
Message string `create:"required"`
Message string `create:"required"`
// swagger:ignore
TopicType string `json:"topic_type" width:"20" nullable:"true" create:"required" update:"user" list:"user"`
ReceivedAt time.Time `nullable:"true" list:"user" get:"user"`
EventId string `width:"128" nullable:"true"`
TopicType string `json:"topic_type" width:"20" nullable:"true" create:"required" update:"user" list:"user"`
SendTimes int
Tag string `width:"16" nullable:"true" index:"true" create:"optional"`
}
const (
@@ -83,72 +81,61 @@ const (
)
func (nm *SNotificationManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input api.NotificationCreateInput) (api.NotificationCreateInput, error) {
if len(input.Tag) > 0 && !utils.IsInStringArray(input.Tag, []string{api.NOTIFICATION_TAG_ALERT}) {
return input, httperrors.NewInputParameterError("invalid tag")
}
if len(input.Contacts) > 0 {
if userCred.IsAllow(rbacscope.ScopeSystem, api.SERVICE_TYPE, nm.KeywordPlural(), policy.PolicyActionPerform, SendByContact).Result.IsDeny() {
return input, httperrors.NewForbiddenError("only admin can send notification by contact")
}
if len(input.Contacts) == 0 {
input.Contacts = []string{""}
}
cTypes := []string{}
if len(input.Contacts) > 0 && !userCred.HasSystemAdminPrivilege() {
return input, httperrors.NewForbiddenError("only admin can send notification by contact")
}
// check robot
if len(input.Robots) > 0 {
input.ContactType = api.ROBOT
robots, err := RobotManager.FetchByIdOrNames(ctx, input.Robots...)
if err != nil {
return input, errors.Wrap(err, "RobotManager.FetchByIdOrNames")
robots := []string{}
for i := range input.Robots {
_robot, err := validators.ValidateModel(userCred, RobotManager, &input.Robots[i])
if err != nil && !input.IgnoreNonexistentReceiver {
return input, err
}
idSet := sets.NewString()
nameSet := sets.NewString()
for i := range robots {
idSet.Insert(robots[i].Id)
nameSet.Insert(robots[i].Name)
}
for _, re := range input.Receivers {
if idSet.Has(re) || nameSet.Has(re) {
continue
if _robot != nil {
robot := _robot.(*SRobot)
if !utils.IsInStringArray(robot.GetId(), robots) {
robots = append(robots, robot.GetId())
}
if !input.IgnoreNonexistentReceiver {
return input, httperrors.NewInputParameterError("no such robot whose id is %q", re)
if !utils.IsInStringArray(robot.Type, cTypes) {
cTypes = append(cTypes, robot.Type)
}
}
input.Robots = idSet.UnsortedList()
if len(input.Robots) == 0 {
return input, httperrors.NewInputParameterError("no valid receiver or contact")
}
}
input.Robots = robots
// check receivers
if len(input.Receivers) > 0 {
receivers, err := ReceiverManager.FetchByIdOrNames(ctx, input.Receivers...)
if err != nil {
return input, errors.Wrap(err, "ReceiverManager.FetchByIDs")
receivers, err := ReceiverManager.FetchByIdOrNames(ctx, input.Receivers...)
if err != nil {
return input, errors.Wrap(err, "ReceiverManager.FetchByIDs")
}
idSet := sets.NewString()
nameSet := sets.NewString()
for i := range receivers {
idSet.Insert(receivers[i].Id)
nameSet.Insert(receivers[i].Name)
}
for _, re := range input.Receivers {
if idSet.Has(re) || nameSet.Has(re) {
continue
}
idSet := sets.NewString()
nameSet := sets.NewString()
for i := range receivers {
idSet.Insert(receivers[i].Id)
nameSet.Insert(receivers[i].Name)
if input.ContactType == api.WEBCONSOLE {
input.Contacts = append(input.Contacts, re)
}
for _, re := range input.Receivers {
if idSet.Has(re) || nameSet.Has(re) {
continue
}
if input.ContactType == api.WEBCONSOLE {
input.Contacts = append(input.Contacts, re)
}
if !input.IgnoreNonexistentReceiver {
return input, httperrors.NewInputParameterError("no such receiver whose uid is %q", re)
}
}
input.Receivers = idSet.UnsortedList()
if len(input.Receivers)+len(input.Contacts) == 0 {
return input, httperrors.NewInputParameterError("no valid receiver or contact")
if !input.IgnoreNonexistentReceiver {
return input, httperrors.NewInputParameterError("no such receiver whose uid is %q", re)
}
}
input.Receivers = idSet.UnsortedList()
if len(input.Receivers)+len(input.Contacts) == 0 {
return input, httperrors.NewInputParameterError("no valid receiver or contact")
}
if len(input.Receivers)+len(input.Contacts)+len(input.Robots) == 0 {
return input, httperrors.NewInputParameterError("no valid receiver or contact")
}
input.ContactType = strings.Join(cTypes, ",")
nowStr := time.Now().Format("2006-01-02 15:04:05")
if len(input.Priority) == 0 {
input.Priority = api.NOTIFICATION_PRIORITY_NORMAL
@@ -159,12 +146,7 @@ func (nm *SNotificationManager) ValidateCreateData(ctx context.Context, userCred
if len(topicRunes) < 10 {
length = len(topicRunes)
}
name := fmt.Sprintf("%s-%s-%s", string(topicRunes[:length]), input.ContactType, nowStr)
var err error
input.Name, err = db.GenerateName(ctx, nm, ownerId, name)
if err != nil {
return input, errors.Wrapf(err, "unable to generate name for %s", name)
}
input.GenerateName = fmt.Sprintf("%s-%s-%s", string(topicRunes[:length]), input.ContactType, nowStr)
return input, nil
}
@@ -198,32 +180,19 @@ func (n *SNotification) CustomizeCreate(ctx context.Context, userCred mcclient.T
}
func (n *SNotification) PostCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) {
if data.Contains("metadata") {
metadata := make(map[string]interface{})
err := data.Unmarshal(&metadata, "metadata")
if err != nil {
log.Errorf("unable to unmarshal to metadata: %v", err)
} else {
n.SetAllMetadata(ctx, metadata, userCred)
}
}
n.SStatusStandaloneResourceBase.PostCreate(ctx, userCred, ownerId, query, data)
n.SetStatus(userCred, api.NOTIFICATION_STATUS_RECEIVED, "")
task, err := taskman.TaskManager.NewTask(ctx, "NotificationSendTask", n, userCred, nil, "", "")
if err != nil {
log.Errorf("NotificationSendTask newTask error %v", err)
} else {
task.ScheduleRun(nil)
n.SetStatus(userCred, api.NOTIFICATION_STATUS_FAILED, "NewTask")
return
}
task.ScheduleRun(nil)
}
// TODO: support project and domain
func (nm *SNotificationManager) PerformEventNotify(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.NotificationManagerEventNotifyInput) (api.NotificationManagerEventNotifyOutput, error) {
var output api.NotificationManagerEventNotifyOutput
// check event
_, err := parseEvent(input.Event)
if err != nil {
return output, httperrors.NewInputParameterError("unable to parse event %q", input.Event)
}
// contact type
contactTypes := input.ContactTypes
cts, err := ConfigManager.allContactType()
@@ -231,11 +200,9 @@ func (nm *SNotificationManager) PerformEventNotify(ctx context.Context, userCred
return output, errors.Wrap(err, "unable to fetch allContactType")
}
if len(contactTypes) == 0 {
contactTypes = intersection(cts, PersonalConfigContactTypes)
contactTypes = append(contactTypes, cts...)
}
// receiver
topic, err := TopicManager.TopicByEvent(input.Event, input.AdvanceDays)
if err != nil {
return output, errors.Wrapf(err, "unable fetch subscriptions by event %q", input.Event)
@@ -301,7 +268,7 @@ func (nm *SNotificationManager) PerformEventNotify(ctx context.Context, userCred
receiverIds = idSet.UnsortedList()
// create event
event, err := EventManager.CreateEvent(ctx, input.Event, topic.Id, message, input.AdvanceDays)
event, err := EventManager.CreateEvent(ctx, input.Event, topic.Id, message, string(input.Action), input.ResourceType, input.AdvanceDays)
if err != nil {
return output, errors.Wrap(err, "unable to create Event")
}
@@ -356,70 +323,6 @@ func (nm *SNotificationManager) needWebconsole(topics []STopic) bool {
return false
}
func (nm *SNotificationManager) createWithWebhookRobots(ctx context.Context, userCred mcclient.TokenCredential, webhookRobotIds []string, priority, eventId string, topicType string) error {
if len(webhookRobotIds) == 0 {
return nil
}
n := &SNotification{
ContactType: api.WEBHOOK,
Priority: priority,
ReceivedAt: time.Now(),
EventId: eventId,
TopicType: topicType,
}
n.Id = db.DefaultUUIDGenerator()
for i := range webhookRobotIds {
_, err := ReceiverNotificationManager.CreateRobot(ctx, userCred, webhookRobotIds[i], n.Id)
if err != nil {
return errors.Wrap(err, "ReceiverNotificationManager.CreateRobot")
}
}
err := nm.TableSpec().Insert(ctx, n)
if err != nil {
return errors.Wrap(err, "unable to insert Notification")
}
n.SetModelManager(nm, n)
task, err := taskman.TaskManager.NewTask(ctx, "NotificationSendTask", n, userCred, nil, "", "")
if err != nil {
log.Errorf("NotificationSendTask newTask error %v", err)
} else {
task.ScheduleRun(nil)
}
return nil
}
func (nm *SNotificationManager) createWithRobots(ctx context.Context, userCred mcclient.TokenCredential, robotIds []string, priority, eventId string, topicType string) error {
if len(robotIds) == 0 {
return nil
}
n := &SNotification{
ContactType: api.ROBOT,
Priority: priority,
ReceivedAt: time.Now(),
EventId: eventId,
TopicType: topicType,
}
n.Id = db.DefaultUUIDGenerator()
for i := range robotIds {
_, err := ReceiverNotificationManager.CreateRobot(ctx, userCred, robotIds[i], n.Id)
if err != nil {
return errors.Wrap(err, "ReceiverNotificationManager.CreateRobot")
}
}
err := nm.TableSpec().Insert(ctx, n)
if err != nil {
return errors.Wrap(err, "unable to insert Notification")
}
n.SetModelManager(nm, n)
task, err := taskman.TaskManager.NewTask(ctx, "NotificationSendTask", n, userCred, nil, "", "")
if err != nil {
log.Errorf("NotificationSendTask newTask error %v", err)
} else {
task.ScheduleRun(nil)
}
return nil
}
func (nm *SNotificationManager) create(ctx context.Context, userCred mcclient.TokenCredential, contactType string, receiverIds, contacts []string, priority, eventId string, topicType string) error {
if len(receiverIds)+len(contacts) == 0 {
return nil
@@ -459,6 +362,99 @@ func (nm *SNotificationManager) create(ctx context.Context, userCred mcclient.To
return nil
}
func (nm *SNotificationManager) createWithWebhookRobots(ctx context.Context, userCred mcclient.TokenCredential, webhookRobotIds []string, priority, eventId string, topicType string) error {
if len(webhookRobotIds) == 0 {
return nil
}
n := &SNotification{
ContactType: api.WEBHOOK,
Priority: priority,
ReceivedAt: time.Now(),
EventId: eventId,
TopicType: topicType,
}
n.Id = db.DefaultUUIDGenerator()
for i := range webhookRobotIds {
_, err := ReceiverNotificationManager.CreateRobot(ctx, userCred, webhookRobotIds[i], n.Id)
if err != nil {
return errors.Wrap(err, "ReceiverNotificationManager.CreateRobot")
}
}
err := nm.TableSpec().Insert(ctx, n)
if err != nil {
return errors.Wrap(err, "unable to insert Notification")
}
n.SetModelManager(nm, n)
task, err := taskman.TaskManager.NewTask(ctx, "NotificationSendTask", n, userCred, nil, "", "")
if err != nil {
return errors.Wrapf(err, "NewTask")
}
return task.ScheduleRun(nil)
}
func (nm *SNotificationManager) createWithRobots(ctx context.Context, userCred mcclient.TokenCredential, robotIds []string, priority, eventId string, topicType string) error {
if len(robotIds) == 0 {
return nil
}
n := &SNotification{
ContactType: api.ROBOT,
Priority: priority,
ReceivedAt: time.Now(),
EventId: eventId,
TopicType: topicType,
}
n.Id = db.DefaultUUIDGenerator()
for i := range robotIds {
_, err := ReceiverNotificationManager.CreateRobot(ctx, userCred, robotIds[i], n.Id)
if err != nil {
return errors.Wrap(err, "ReceiverNotificationManager.CreateRobot")
}
}
err := nm.TableSpec().Insert(ctx, n)
if err != nil {
return errors.Wrap(err, "unable to insert Notification")
}
n.SetModelManager(nm, n)
task, err := taskman.TaskManager.NewTask(ctx, "NotificationSendTask", n, userCred, nil, "", "")
if err != nil {
log.Errorf("NotificationSendTask newTask error %v", err)
} else {
task.ScheduleRun(nil)
}
return nil
}
func (n *SNotification) Create(ctx context.Context, userCred mcclient.TokenCredential, receiverIds, contacts []string) error {
if len(receiverIds)+len(contacts) == 0 {
return nil
}
n.Id = db.DefaultUUIDGenerator()
err := NotificationManager.TableSpec().Insert(ctx, n)
if err != nil {
return errors.Wrap(err, "unable to insert Notification")
}
for i := range receiverIds {
_, err := ReceiverNotificationManager.Create(ctx, userCred, receiverIds[i], n.Id)
if err != nil {
return errors.Wrap(err, "ReceiverNotificationManager.Create")
}
}
for i := range contacts {
_, err := ReceiverNotificationManager.CreateContact(ctx, userCred, contacts[i], n.Id)
if err != nil {
return errors.Wrap(err, "ReceiverNotificationManager.CreateContact")
}
}
task, err := taskman.TaskManager.NewTask(ctx, "NotificationSendTask", n, userCred, nil, "", "")
if err != nil {
log.Errorf("NotificationSendTask newTask error %v", err)
} else {
task.ScheduleRun(nil)
}
return nil
}
func (nm *SNotificationManager) FetchCustomizeColumns(
ctx context.Context,
userCred mcclient.TokenCredential,
@@ -467,8 +463,8 @@ func (nm *SNotificationManager) FetchCustomizeColumns(
fields stringutils2.SSortedStrings,
isList bool,
) []api.NotificationDetails {
log.Infoln("this is objs:", objs)
rows := make([]api.NotificationDetails, len(objs))
resRows := nm.SStatusStandaloneResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
var err error
@@ -535,9 +531,10 @@ func (n *SNotification) getMoreDetails(ctx context.Context, userCred mcclient.To
if err != nil {
return out, err
}
p, err := n.TemplateStore().FillWithTemplate(ctx, lang, nn)
// p, err := n.TemplateStore().FillWithTemplate(ctx, lang, nn)
p, _ := LocalTemplateManager.FillWithTemplate(ctx, lang, nn)
if err != nil {
return out, errors.Wrap(err, "TemplateStore().FillWithTemplate")
return out, err
}
out.Title = p.Title
out.Content = p.Message
@@ -546,14 +543,14 @@ func (n *SNotification) getMoreDetails(ctx context.Context, userCred mcclient.To
// get receive details
out.ReceiveDetails, err = n.receiveDetails(userCred, scope)
if err != nil {
return out, errors.Wrap(err, "receiveDetails")
return out, err
}
return out, nil
}
func (n *SNotification) Notification() (notifyv2.SNotification, error) {
func (n *SNotification) Notification() (api.SsNotification, error) {
if n.EventId == "" {
return notifyv2.SNotification{
return api.SsNotification{
ContactType: n.ContactType,
Topic: n.Topic,
Message: n.Message,
@@ -561,10 +558,10 @@ func (n *SNotification) Notification() (notifyv2.SNotification, error) {
}
event, err := EventManager.GetEvent(n.EventId)
if err != nil {
return notifyv2.SNotification{}, err
return api.SsNotification{}, err
}
e, _ := parseEvent(event.Event)
return notifyv2.SNotification{
return api.SsNotification{
ContactType: n.ContactType,
Topic: n.Topic,
Message: event.Message,
@@ -617,31 +614,6 @@ func (n *SNotification) AddOne() error {
return err
}
const (
NOTIFY_RECEIVED = "received" // Received a task about sending a notification
NOTIFY_SENT = "sent" // Nofity module has sent notification, but result unkown
NOTIFY_OK = "sent_ok" // Notification was sent successfully
NOTIFY_FAIL = "sent_fail" // That sent a notification is failed
NOTIFY_REMOVED = "removed"
)
func (self *SNotificationManager) singleRowLineQuery(sqlStr string, dest ...interface{}) error {
q := sqlchemy.NewRawQuery(sqlStr)
rows, err := q.Rows()
if err != nil {
return errors.Wrap(err, "q.Rows")
}
defer rows.Close()
for rows.Next() {
err := rows.Scan(dest...)
if err != nil {
return errors.Wrap(err, "rows.Scan")
}
return nil
}
return sql.ErrNoRows
}
func (self *SNotificationManager) InitializeData() error {
return dataCleaning(self.TableSpec().Name())
}
@@ -650,7 +622,7 @@ func dataCleaning(tableName string) error {
now := time.Now()
monthsDaysAgo := now.AddDate(0, -1, 0).Format("2006-01-02 15:04:05")
sqlStr := fmt.Sprintf(
"delete from %s where created_at < '%s'",
"delete from %s where deleted = 0 and created_at < '%s'",
tableName,
monthsDaysAgo,
)
@@ -664,117 +636,6 @@ func dataCleaning(tableName string) error {
return nil
}
func (self *SNotificationManager) dataMigration() error {
// check
sqlStr := fmt.Sprintf(
"select count(*) as total from (select cluster_id from %s where status='%s' and contact_type='webconsole' group by cluster_id) as cluster",
oldmodels.NotificationManager.TableSpec().Name(),
NOTIFY_REMOVED,
)
var count int
err := self.singleRowLineQuery(sqlStr, &count)
if err != nil {
return err
}
if count >= options.Options.MaxSyncNotification {
return nil
}
limitTimeStr := time.Now().Add(time.Duration(-30) * time.Hour * 24).Format("2006-01-02 15:04:05")
// get min received_at
var minReceivedAt time.Time
sqlStr = fmt.Sprintf(
"select min(received_at) as min_received_at from (select received_at from %s where received_at > '%s' and contact_type = 'webconsole' group by cluster_id order by received_at desc limit %d) as cluster",
oldmodels.NotificationManager.TableSpec().Name(),
limitTimeStr,
options.Options.MaxSyncNotification,
)
err = self.singleRowLineQuery(sqlStr, &minReceivedAt)
if err != nil {
return err
}
log.Infof("minReceivedAt: %s", minReceivedAt)
ctx := context.Background()
q := oldmodels.NotificationManager.Query().Equals("contact_type", api.WEBCONSOLE).GT("received_at", minReceivedAt).NotEquals("status", NOTIFY_REMOVED)
n := q.Count()
log.Infof("total %d notifications to sync", n)
oldNotifications := make([]oldmodels.SNotification, 0, n)
err = db.FetchModelObjects(oldmodels.NotificationManager, q, &oldNotifications)
if err != nil {
return errors.Wrap(err, "db.FetchModelObjects")
}
// build cluster=>Notification
cnMap := make(map[string][]*oldmodels.SNotification)
for i := range oldNotifications {
clusterId := oldNotifications[i].ClusterID
if _, ok := cnMap[clusterId]; !ok {
cnMap[clusterId] = make([]*oldmodels.SNotification, 0, 2)
}
cnMap[clusterId] = append(cnMap[clusterId], &oldNotifications[i])
}
for _, oldNotifications := range cnMap {
oldNotificaion := oldNotifications[0]
newNotification := SNotification{
ContactType: oldNotificaion.ContactType,
Topic: oldNotificaion.Topic,
Priority: oldNotificaion.Priority,
Message: oldNotificaion.Msg,
ReceivedAt: oldNotificaion.ReceivedAt,
}
newNotification.Id = db.DefaultUUIDGenerator()
statusMap := make(map[string]int, 4)
for _, oldNotificaion := range oldNotifications {
rn := SReceiverNotification{
ReceiverID: oldNotificaion.UID,
NotificationID: newNotification.Id,
SendAt: oldNotificaion.SendAt,
SendBy: oldNotificaion.SendBy,
Status: oldNotificaion.Status,
}
if rn.Status == NOTIFY_SENT {
rn.Status = api.NOTIFICATION_STATUS_SENDING
}
statusMap[rn.Status] += 1
err := ReceiverNotificationManager.TableSpec().Insert(ctx, &rn)
if err != nil {
return errors.Wrap(err, "TableSpec().Insert")
}
}
switch {
case statusMap[api.RECEIVER_NOTIFICATION_OK] == len(oldNotifications):
newNotification.Status = api.NOTIFICATION_STATUS_OK
case statusMap[api.RECEIVER_NOTIFICATION_RECEIVED] == len(oldNotifications):
newNotification.Status = api.NOTIFICATION_STATUS_RECEIVED
case statusMap[api.RECEIVER_NOTIFICATION_FAIL] == len(oldNotifications):
newNotification.Status = api.NOTIFICATION_STATUS_FAILED
case statusMap[api.RECEIVER_NOTIFICATION_FAIL] == 0 && statusMap[api.RECEIVER_NOTIFICATION_SENT] > 0:
newNotification.Status = api.NOTIFICATION_STATUS_SENDING
default:
newNotification.Status = api.NOTIFICATION_STATUS_PART_OK
}
err := self.TableSpec().InsertOrUpdate(ctx, &newNotification)
if err != nil {
return errors.Wrap(err, "TableSpec().InsertOrUpdate")
}
// mark removed
for _, oldNotificaion := range oldNotifications {
_, err := db.Update(oldNotificaion, func() error {
oldNotificaion.Status = NOTIFY_REMOVED
return nil
})
if err != nil {
return errors.Wrap(err, "Delete")
}
}
}
return nil
}
// 通知消息列表
func (nm *SNotificationManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, input api.NotificationListInput) (*sqlchemy.SQuery, error) {
q, err := nm.SStandaloneResourceBaseManager.ListItemFilter(ctx, q, userCred, input.StandaloneResourceListInput)
@@ -817,9 +678,22 @@ func (nm *SNotificationManager) ReSend(ctx context.Context, userCred mcclient.To
}
}
func (n *SNotification) TemplateStore() notifyv2.ITemplateStore {
func (n *SNotification) FillWithTemplate(ctx context.Context, lang string, no api.SsNotification) (api.SendParams, error) {
if len(n.EventId) == 0 || n.ContactType == api.MOBILE {
return TemplateManager
return TemplateManager.FillWithTemplate(ctx, lang, no)
}
return LocalTemplateManager
return LocalTemplateManager.FillWithTemplate(ctx, lang, no)
}
func (n *SNotification) GetNotOKReceivers() ([]SReceiver, error) {
ret := []SReceiver{}
q := ReceiverManager.Query().IsTrue("enabled")
sq := ReceiverNotificationManager.Query().Equals("notification_id", n.Id).NotEquals("status", api.RECEIVER_NOTIFICATION_OK).Equals("receiver_type", api.RECEIVER_TYPE_USER).SubQuery()
q = q.Join(sq, sqlchemy.Equals(q.Field("id"), sq.Field("receiver_id")))
err := db.FetchModelObjects(ReceiverManager, q, &ret)
return ret, err
}
func (n *SNotification) TaskInsert() error {
return NotificationManager.TableSpec().Insert(context.Background(), n)
}
+73
View File
@@ -0,0 +1,73 @@
// 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 (
api "yunion.io/x/onecloud/pkg/apis/notify"
)
type ISenderDriver interface {
GetSenderType() string
Send(args api.SendParams) error
ValidateConfig(api.NotifyConfig) (string, error)
ContactByMobile(mobile, domainId string) (string, error)
IsRobot() bool
IsPersonal() bool
IsSystemConfigContactType() bool
IsValid() bool
IsPullType() bool
GetAccessToken() error
}
var (
driverTable = make(map[string]ISenderDriver)
)
func Register(driver ISenderDriver) {
driverTable[driver.GetSenderType()] = driver
}
func GetSenderTypes() []string {
ret := []string{}
for k := range driverTable {
ret = append(ret, k)
}
return ret
}
func GetRobotTypes() []string {
ret := []string{}
for k := range driverTable {
if driverTable[k].IsRobot() {
ret = append(ret, k)
}
}
return ret
}
func GetValidPersonalSenderTypes() []string {
ret := []string{}
for k := range driverTable {
if driverTable[k].IsValid() && driverTable[k].IsPersonal() {
ret = append(ret, k)
}
}
return ret
}
func GetDriver(sendType string) ISenderDriver {
driver, _ := driverTable[sendType]
return driver
}
File diff suppressed because it is too large Load Diff
+38 -10
View File
@@ -19,6 +19,8 @@ import (
"net/http"
"time"
"yunion.io/x/pkg/errors"
api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/appsrv"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
@@ -71,6 +73,14 @@ func (self *SReceiverNotificationManager) InitializeData() error {
return dataCleaning(self.TableSpec().Name())
}
func (self *SReceiverNotification) GetReceiver() (*SReceiver, error) {
recv, err := ReceiverManager.FetchById(self.ReceiverID)
if err != nil {
return nil, errors.Wrapf(err, "with id %s", self.ReceiverID)
}
return recv.(*SReceiver), nil
}
func (rnm *SReceiverNotificationManager) Create(ctx context.Context, userCred mcclient.TokenCredential, receiverID, notificationID string) (*SReceiverNotification, error) {
rn := &SReceiverNotification{
ReceiverID: receiverID,
@@ -119,16 +129,22 @@ func (rnm *SReceiverNotificationManager) SetHandlerProcessTimeout(info *appsrv.S
return -time.Second
}
// func (rn *SReceiverNotification) Receiver() (*SReceiver, error) {
// q := ReceiverManager.Query().Equals("id", rn.ReceiverID)
// var receiver SReceiver
// err := q.First(&receiver)
// if err != nil {
// return nil, err
// }
// receiver.SetModelManager(ReceiverManager, &receiver)
// return &receiver, nil
// }
func (rn *SReceiverNotification) Receiver() (IReceiver, error) {
switch rn.ReceiverType {
case api.RECEIVER_TYPE_USER:
return rn.receiver()
case api.RECEIVER_TYPE_CONTACT:
return &SContact{contact: rn.Contact}, nil
case api.RECEIVER_TYPE_ROBOT:
return rn.robot()
default:
// compatible
if rn.ReceiverID != "" && rn.ReceiverID != ReceiverIdDefault {
return rn.receiver()
}
return &SContact{contact: rn.Contact}, nil
}
}
func (rn *SReceiverNotification) receiver() (*SReceiver, error) {
q := ReceiverManager.Query().Equals("id", rn.ReceiverID)
@@ -152,6 +168,7 @@ func (rn *SReceiverNotification) robot() (*SRobot, error) {
return &robot, nil
}
/*
func (rn *SReceiverNotification) Receiver() (IReceiver, error) {
switch rn.ReceiverType {
case api.RECEIVER_TYPE_USER:
@@ -168,6 +185,7 @@ func (rn *SReceiverNotification) Receiver() (IReceiver, error) {
return &SContact{contact: rn.Contact}, nil
}
}
*/
func (rn *SReceiverNotification) BeforeSend(ctx context.Context, sendTime time.Time) error {
if sendTime.IsZero() {
@@ -195,6 +213,8 @@ func (rn *SReceiverNotification) AfterSend(ctx context.Context, success bool, re
}
type IReceiver interface {
IsRobot() bool
IsReceiver() bool
IsEnabled() bool
GetDomainId() string
IsEnabledContactType(string) (bool, error)
@@ -234,3 +254,11 @@ type SContact struct {
func (s *SContact) GetContact(_ string) (string, error) {
return s.contact, nil
}
func (s *SContact) IsRobot() bool {
return false
}
func (s *SContact) IsReceiver() bool {
return false
}
+39 -165
View File
@@ -16,7 +16,7 @@ package models
import (
"context"
"strings"
"fmt"
"golang.org/x/text/language"
@@ -24,18 +24,15 @@ import (
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/tristate"
"yunion.io/x/pkg/util/rbacscope"
"yunion.io/x/pkg/utils"
"yunion.io/x/sqlchemy"
"yunion.io/x/onecloud/pkg/apis"
idenapi "yunion.io/x/onecloud/pkg/apis/identity"
"yunion.io/x/onecloud/pkg/apis/notify"
api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
rpcapi "yunion.io/x/onecloud/pkg/notify/rpc/apis"
"yunion.io/x/onecloud/pkg/util/stringutils2"
)
@@ -67,135 +64,7 @@ type SRobot struct {
Lang string `width:"16" nullable:"false" create:"required" update:"user" get:"user" list:"user"`
}
func (rm *SRobotManager) fetchSystemProjectId(ctx context.Context) (string, error) {
tenant, err := db.TenantCacheManager.FetchTenantByNameInDomain(ctx, "system", idenapi.DEFAULT_DOMAIN_ID)
if err != nil {
return "", err
}
return tenant.Id, nil
}
func (rm *SRobotManager) InitializeData() error {
log.Infof("start to init data for notify robot")
// init empty projectId robot
ctx := context.WithValue(context.Background(), "from", "init")
systemId, err := rm.fetchSystemProjectId(ctx)
if err != nil {
return errors.Wrap(err, "unable to fetch system project id")
}
rq := RobotManager.Query()
rq = rq.Filter(sqlchemy.OR(sqlchemy.IsEmpty(rq.Field("tenant_id")), sqlchemy.Equals(rq.Field("tenant_id"), "system")))
npRobots := make([]SRobot, 0)
err = db.FetchModelObjects(RobotManager, rq, &npRobots)
if err != nil {
return errors.Wrap(err, "unable to fetch robots without project")
}
for i := range npRobots {
robot := &npRobots[i]
_, err := db.Update(robot, func() error {
robot.ProjectId = systemId
robot.ProjectSrc = "local"
return nil
})
if err != nil {
return errors.Wrapf(err, "unable to update robot %s", robot.Id)
}
}
// convert robot config
q := ConfigManager.Query().In("type", append(RobotContactTypes, api.WEBHOOK))
var configs []SConfig
err = db.FetchModelObjects(ConfigManager, q, &configs)
if err != nil {
return err
}
if len(configs) == 0 {
return nil
}
robots := make([]SRobot, 0, len(configs))
for i := range configs {
webhook, _ := configs[i].Content.GetString("webhook")
robot := SRobot{
Address: webhook,
Lang: "zh_CN",
}
robot.IsPublic = true
robot.PublicScope = string(rbacscope.ScopeSystem)
robot.DomainId = idenapi.DEFAULT_DOMAIN_ID
robot.ProjectId = systemId
robot.ProjectSrc = "local"
robot.Status = api.RECEIVER_STATUS_READY
switch configs[i].Type {
case api.FEISHU_ROBOT:
robot.Type = api.ROBOT_TYPE_FEISHU
robot.Name = "Feishu Robot"
case api.DINGTALK_ROBOT:
robot.Type = api.ROBOT_TYPE_DINGTALK
robot.Name = "Dingtalk Robot"
case api.WORKWX_ROBOT:
robot.Type = api.ROBOT_TYPE_WORKWX
robot.Name = "Workwx Robot"
case api.WEBHOOK:
robot.Type = api.ROBOT_TYPE_WEBHOOK
robot.Name = "Webhook"
addresses := strings.Split(robot.Address, ",")
for i := 1; i < len(addresses); i++ {
robotn := robot
robotn.Address = strings.TrimSpace(addresses[i])
robots = append(robots, robotn)
}
robot.Address = addresses[0]
default:
continue
}
robots = append(robots, robot)
}
var webhookRobotId string
// insert new robot
for i := range robots {
err := rm.TableSpec().Insert(ctx, &robots[i])
if err != nil {
return err
}
if robots[i].Type == api.ROBOT_TYPE_WEBHOOK {
webhookRobotId = robots[i].Id
}
}
// delete old configs
for i := range configs {
config := &configs[i]
_, err := db.Update(config, func() error {
return config.MarkDelete()
})
if err != nil {
return err
}
}
// add webhook Robot to subscriber
if len(webhookRobotId) > 0 {
topics := make([]STopic, 0, 3)
q = TopicManager.Query().In("name", []string{DefaultResourceCreateDelete, DefaultResourceChangeConfig, DefaultResourceUpdate})
err := db.FetchModelObjects(TopicManager, q, &topics)
if err != nil {
return errors.Wrap(err, "unable to fetch topics")
}
// create subscribers
for i := range topics {
subscriber := SSubscriber{
TopicID: topics[i].Id,
Type: api.SUBSCRIBER_TYPE_ROBOT,
Identification: webhookRobotId,
ResourceScope: "system",
Scope: "system",
}
subscriber.Enabled = tristate.True
err := SubscriberManager.TableSpec().Insert(ctx, &subscriber)
if err != nil {
return errors.Wrapf(err, "unable to create subscriber %s", jsonutils.Marshal(subscriber))
}
}
}
return nil
}
var RobotList = []string{api.FEISHU_ROBOT, api.DINGTALK_ROBOT, api.WORKWX_ROBOT, api.WEBHOOK, api.WEBHOOK_ROBOT}
func (rm *SRobotManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input api.RobotCreateInput) (api.RobotCreateInput, error) {
var err error
@@ -204,41 +73,35 @@ func (rm *SRobotManager) ValidateCreateData(ctx context.Context, userCred mcclie
return input, errors.Wrap(err, "SSharableVirtualResourceBaseManager.ValidateCreateData")
}
// check type
if !utils.IsInStringArray(input.Type, []string{api.ROBOT_TYPE_FEISHU, api.ROBOT_TYPE_WORKWX, api.ROBOT_TYPE_DINGTALK, api.ROBOT_TYPE_WEBHOOK}) {
return input, httperrors.NewInputParameterError("unkown type %q", input.Type)
if !utils.IsInStringArray(fmt.Sprintf("%s-robot", input.Type), GetRobotTypes()) {
return input, httperrors.NewInputParameterError("unkown type %s support: %s", input.Type, GetRobotTypes())
}
// check lang
if input.Lang == "" {
input.Lang = "zh_CN"
} else {
_, err = language.Parse(input.Lang)
if err != nil {
return input, httperrors.NewInputParameterError("invalid lang %q: %s", input.Lang, err.Error())
}
}
// check Address
records, err := NotifyService.SendRobotMessage(ctx, input.Type, []*rpcapi.SReceiver{
{
Contact: input.Address,
DomainId: input.ProjectDomainId,
},
}, "Validate", "This is a verification message, please ignore.")
_, err = language.Parse(input.Lang)
if err != nil {
return input, errors.Wrap(err, "unable to validate address")
}
if len(records) > 0 {
return input, httperrors.NewInputParameterError("invalid address: %s", records[0].Reason)
input.SetEnabled()
input.Status = api.ROBOT_STATUS_READY
driver := GetDriver(fmt.Sprintf("%s-robot", input.Type))
err = driver.Send(api.SendParams{
Receivers: api.SNotifyReceiver{
Contact: input.Address,
DomainId: input.ProjectDomainId,
},
Title: "Validate",
Message: "This is a verification message, please ignore.",
})
if err != nil {
return input, err
}
return input, nil
}
func (r *SRobot) Receiver() *rpcapi.SReceiver {
return &rpcapi.SReceiver{
Contact: r.Address,
DomainId: r.DomainId,
}
}
func (rm *SRobotManager) FetchCustomizeColumns(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, objs []interface{}, fields stringutils2.SSortedStrings, isList bool) []api.RobotDetails {
sRows := rm.SSharableVirtualResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
rows := make([]api.RobotDetails, len(objs))
@@ -281,18 +144,11 @@ func (r *SRobot) ValidateUpdateData(ctx context.Context, userCred mcclient.Token
}
if len(input.Address) > 0 {
// check Address
records, err := NotifyService.SendRobotMessage(ctx, r.Type, []*rpcapi.SReceiver{
{
Contact: input.Address,
DomainId: r.DomainId,
},
}, "Validate", "This is a verification message, please ignore.")
dirver := GetDriver(r.Type)
err := dirver.Send(api.SendParams{})
if err != nil {
return input, errors.Wrap(err, "unable to validate address")
}
if len(records) > 0 {
return input, httperrors.NewInputParameterError("invalid address: %s", records[0].Reason)
}
}
return input, nil
}
@@ -330,7 +186,15 @@ func (r *SRobot) IsEnabledContactType(ctype string) (bool, error) {
}
func (r *SRobot) IsVerifiedContactType(ctype string) (bool, error) {
return ctype == api.ROBOT || ctype == api.WEBHOOK, nil
return utils.IsInStringArray(ctype, RobotList), nil
}
func (r *SRobot) IsRobot() bool {
return true
}
func (r *SRobot) IsReceiver() bool {
return false
}
func (r *SRobot) GetContact(ctype string) (string, error) {
@@ -384,3 +248,13 @@ func (r *SRobot) PostDelete(ctx context.Context, userCred mcclient.TokenCredenti
}
}
}
func GetRobotTypeById(id string) (string, error) {
imode, err := RobotManager.FetchById(id)
if err != nil {
return "", errors.Wrap(err, "FetchById")
}
log.Infoln("this is robot:", jsonutils.Marshal(imode))
robot := imode.(*SRobot)
return robot.Type, nil
}
+41
View File
@@ -0,0 +1,41 @@
// 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 (
api "yunion.io/x/onecloud/pkg/apis/notify"
)
type ISmsDriver interface {
Name() string
Send(args api.SSMSSendParams, isVerify bool, config *api.NotifyConfig) error
Verify(config *api.NotifyConfig) error
}
var (
smsdriverTable = make(map[string]ISmsDriver)
)
func SMSRegister(driver ISmsDriver) {
smsdriverTable[driver.Name()] = driver
}
func GetSMSDriver(smsDriver string) ISmsDriver {
driver, isExist := smsdriverTable[smsDriver]
if isExist {
return driver
}
return nil
}
+17
View File
@@ -75,6 +75,7 @@ type SSubscriberManager struct {
db.SEnabledResourceBaseManager
}
// 消息订阅接收人
type SSubscriber struct {
db.SStandaloneAnonResourceBase
db.SEnabledResourceBase
@@ -108,6 +109,22 @@ func (sm *SSubscriberManager) validateReceivers(ctx context.Context, receivers [
return reIds, nil
}
func (self *SSubscriber) GetEnabledReceivers() ([]SReceiver, error) {
q := ReceiverManager.Query().IsTrue("enabled")
sq := SubscriberReceiverManager.Query().SubQuery()
q = q.Join(sq, sqlchemy.Equals(q.Field("id"), sq.Field("receiver_id"))).Filter(sqlchemy.Equals(sq.Field("subscriber_id"), self.Id))
ret := []SReceiver{}
return ret, db.FetchModelObjects(ReceiverManager, q, &ret)
}
func (self *SSubscriber) GetRobot() (*SRobot, error) {
robot, err := RobotManager.FetchById(self.Identification)
if err != nil {
return nil, errors.Wrapf(err, "RobotManager.FetchById(%s)", self.Identification)
}
return robot.(*SRobot), nil
}
func (sm *SSubscriberManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input api.SubscriberCreateInput) (api.SubscriberCreateInput, error) {
var err error
// permission check
+5 -5
View File
@@ -20,6 +20,7 @@ import (
"database/sql"
"encoding/json"
"fmt"
"html"
"io/ioutil"
"path/filepath"
"strings"
@@ -37,9 +38,7 @@ import (
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/auth"
notifyv2 "yunion.io/x/onecloud/pkg/notify"
"yunion.io/x/onecloud/pkg/notify/options"
"yunion.io/x/onecloud/pkg/notify/rpc/apis"
)
type STemplateManager struct {
@@ -68,11 +67,11 @@ type STemplate struct {
db.SStandaloneAnonResourceBase
ContactType string `width:"16" nullable:"false" create:"required" update:"user" list:"user"`
Topic string `width:"20" nullable:"false" create:"required" update:"user" list:"user"`
Topic string `width:"128" 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" update:"user"`
Content string `length:"text" nullable:"true" create:"required" get:"user" list:"user" update:"user"`
Lang string `width:"8" charset:"ascii" nullable:"false" list:"user" update:"user" create:"optional"`
Example string `nullable:"true" create:"optional" get:"user" list:"user" update:"user"`
}
@@ -215,7 +214,7 @@ func (tm *STemplateManager) InitializeData() error {
// FillWithTemplate 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) FillWithTemplate(ctx context.Context, lang string, no notifyv2.SNotification) (params apis.SendParams, err error) {
func (tm *STemplateManager) FillWithTemplate(ctx context.Context, lang string, no api.SsNotification) (params api.SendParams, err error) {
if len(lang) == 0 {
params.Title = no.Topic
params.Message = no.Message
@@ -259,6 +258,7 @@ func (tm *STemplateManager) FillWithTemplate(ctx context.Context, lang string, n
return
}
}
params.Message = html.UnescapeString(params.Message)
return
}
+89 -9
View File
@@ -31,6 +31,7 @@ import (
"yunion.io/x/onecloud/pkg/apis/notify"
api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/util/bitmap"
@@ -68,6 +69,7 @@ func init() {
TopicManager.SetVirtualObject(TopicManager)
}
// 消息订阅
type STopic struct {
db.SEnabledStatusStandaloneResourceBase
@@ -197,6 +199,7 @@ func (sm *STopicManager) InitializeData() error {
)
t.addAction(notify.ActionChangeConfig)
t.Type = notify.TOPIC_TYPE_RESOURCE
t.Results = tristate.True
case DefaultResourceUpdate:
t.addResources(
notify.TOPIC_RESOURCE_SERVER,
@@ -251,6 +254,7 @@ func (sm *STopicManager) InitializeData() error {
t.addResources(notify.TOPIC_RESOURCE_SCHEDULEDTASK)
t.addAction(notify.ActionExecute)
t.Type = notify.TOPIC_TYPE_AUTOMATED_PROCESS
t.Results = tristate.True
case DefaultScalingPolicyExecute:
t.addResources(notify.TOPIC_RESOURCE_SCALINGPOLICY)
t.addAction(notify.ActionExecute)
@@ -423,6 +427,7 @@ func (sm *STopicManager) InitializeData() error {
t.Results = tristate.False
t.Type = notify.TOPIC_TYPE_RESOURCE
}
if topic == nil {
err := sm.TableSpec().Insert(ctx, t)
if err != nil {
@@ -542,18 +547,42 @@ func (s *STopic) getActions() []notify.SAction {
return actions
}
func (sm *STopicManager) TopicByEvent(eventStr string, advanceDays int) (*STopic, error) {
topics, err := sm.TopicsByEvent(eventStr, advanceDays)
func (sm *STopicManager) GetTopicByEvent(resourceType string, action notify.SAction, isFailed notify.SResult, advanceDays int) (*STopic, error) {
topics, err := sm.GetTopicsByEvent(resourceType, action, isFailed, advanceDays)
if err != nil {
return nil, err
}
if len(topics) == 1 {
return &topics[0], nil
return nil, errors.Wrapf(err, "GetTopicsByEvent")
}
if len(topics) == 0 {
return nil, errors.Wrapf(cloudprovider.ErrNotFound, "eventStr:%s,advanceDays:%d", eventStr, advanceDays)
return nil, httperrors.NewResourceNotFoundError("no available topic found by %s %s", action, resourceType)
}
return nil, errors.Wrapf(cloudprovider.ErrDuplicateId, "eventStr:%s,advanceDays:%d", eventStr, advanceDays)
// free memory in time
if len(topics) > 1 {
return nil, httperrors.NewResourceNotFoundError("duplicates %d topics found by %s %s", len(topics), action, resourceType)
}
return &topics[0], nil
}
func (sm *STopicManager) GetTopicsByEvent(resourceType string, action notify.SAction, isFailed notify.SResult, advanceDays int) ([]STopic, error) {
resourceV := converter.resourceValue(resourceType)
if resourceV < 0 {
return nil, fmt.Errorf("unknow resource type %s", resourceType)
}
actionV := converter.actionValue(action)
if actionV < 0 {
return nil, fmt.Errorf("unkonwn action %s", action)
}
q := sm.Query().Equals("advance_days", advanceDays)
if isFailed == api.ResultSucceed {
q = q.Equals("results", true)
} else {
q = q.Equals("results", false)
}
q = q.Equals("enabled", true)
q = q.Filter(sqlchemy.GT(sqlchemy.AND_Val("", q.Field("resources"), 1<<resourceV), 0))
q = q.Filter(sqlchemy.GT(sqlchemy.AND_Val("", q.Field("actions"), 1<<actionV), 0))
topics := []STopic{}
err := db.FetchModelObjects(sm, q, &topics)
return topics, err
}
func (sm *STopicManager) TopicsByEvent(eventStr string, advanceDays int) ([]STopic, error) {
@@ -561,7 +590,6 @@ func (sm *STopicManager) TopicsByEvent(eventStr string, advanceDays int) ([]STop
if err != nil {
return nil, errors.Wrapf(err, "unable to parse event %q", event)
}
log.Infof("event: %s", event.String())
resourceV := converter.resourceValue(event.ResourceType())
if resourceV < 0 {
log.Warningf("unknown resource type: %s", event.ResourceType())
@@ -768,3 +796,55 @@ func (rc *sConverter) action(actionValue int) notify.SAction {
}
return a.(notify.SAction)
}
func (self *STopic) StartMessageSendTask(ctx context.Context, userCred mcclient.TokenCredential, input api.NotificationManagerEventNotifyInput) error {
params := jsonutils.Marshal(input).(*jsonutils.JSONDict)
task, err := taskman.TaskManager.NewTask(ctx, "TopicMessageSendTask", self, userCred, params, "", "")
if err != nil {
return errors.Wrapf(err, "NewTask")
}
return task.ScheduleRun(nil)
}
func (self *STopic) CreateEvent(ctx context.Context, resType, action, message string) (*SEvent, error) {
eve := &SEvent{
Message: message,
ResourceType: resType,
Action: action,
AdvanceDays: self.AdvanceDays,
TopicId: self.Id,
}
return eve, EventManager.TableSpec().Insert(ctx, eve)
}
func (self *STopic) GetEnabledSubscribers(domainId, projectId string) ([]SSubscriber, error) {
q := SubscriberManager.Query().Equals("topic_id", self.Id).IsTrue("enabled")
q = q.Filter(sqlchemy.OR(
sqlchemy.AND(
sqlchemy.Equals(q.Field("resource_scope"), api.SUBSCRIBER_SCOPE_PROJECT),
sqlchemy.Equals(q.Field("resource_attribution_id"), projectId),
),
sqlchemy.AND(
sqlchemy.Equals(q.Field("resource_scope"), api.SUBSCRIBER_SCOPE_DOMAIN),
sqlchemy.Equals(q.Field("resource_attribution_id"), domainId),
),
sqlchemy.Equals(q.Field("resource_scope"), api.SUBSCRIBER_SCOPE_SYSTEM),
))
ret := []SSubscriber{}
err := db.FetchModelObjects(SubscriberManager, q, &ret)
return ret, err
}
func (sm *STopicManager) TopicByEvent(eventStr string, advanceDays int) (*STopic, error) {
topics, err := sm.TopicsByEvent(eventStr, advanceDays)
if err != nil {
return nil, err
}
if len(topics) == 1 {
return &topics[0], nil
}
if len(topics) == 0 {
return nil, errors.Wrapf(cloudprovider.ErrNotFound, "eventStr:%s,advanceDays:%d", eventStr, advanceDays)
}
return nil, errors.Wrapf(cloudprovider.ErrDuplicateId, "eventStr:%s,advanceDays:%d", eventStr, advanceDays)
}
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package sender
package models
import "yunion.io/x/onecloud/pkg/appsrv"
-98
View File
@@ -1,98 +0,0 @@
// 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 oldmodels
import (
"context"
"fmt"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/onecloud/pkg/apis"
"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) {
input := apis.ResourceBaseCreateInput{}
err := data.Unmarshal(&input)
if err != nil {
return nil, httperrors.NewInternalServerError("unmarshal ResourceBaseCreateInput fail %s", err)
}
input, err = self.SResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, input)
if err != nil {
return nil, err
}
data.Update(jsonutils.Marshal(input))
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
}
-44
View File
@@ -1,44 +0,0 @@
// 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 oldmodels
type SConfigManager struct {
SStatusStandaloneResourceBaseManager
}
var ConfigManager *SConfigManager
func init() {
ConfigManager = &SConfigManager{
SStatusStandaloneResourceBaseManager: NewStatusStandaloneResourceBaseManager(
SConfig{},
"notify_t_config",
"oldconfig",
"oldconfigs",
),
}
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:"256" nullable:"false" create:"required" list:"user"`
}
-53
View File
@@ -1,53 +0,0 @@
// 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 oldmodels
import (
"time"
)
type SContactManager struct {
SStatusStandaloneResourceBaseManager
}
var ContactManager *SContactManager
func init() {
ContactManager = &SContactManager{
SStatusStandaloneResourceBaseManager: NewStatusStandaloneResourceBaseManager(
SContact{},
"notify_t_contacts",
"contact",
"contacts",
),
}
ContactManager.SetVirtualObject(ContactManager)
}
const (
CONTACT_INIT = "init" // Contact's status is init which means no verifying
CONTACT_VERIFYING = "verifying" // Contact's status is verifying
CONTACT_VERIFIED = "verified" // Contact's status is verified
)
type SContact struct {
SStatusStandaloneResourceBase
UID string `width:"128" nullable:"false" create:"required" update:"user" list:"user" get:"user"`
ContactType string `width:"16" nullable:"false" create:"required" update:"user"`
Contact string `width:"64" nullable:"false" create:"required" update:"user"`
Enabled string `width:"5" nullable:"false" default:"1" create:"optional" update:"user"`
VerifiedAt time.Time `update:"user"`
}
-52
View File
@@ -1,52 +0,0 @@
// 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 oldmodels
import (
"time"
)
type SNotificationManager struct {
SStatusStandaloneResourceBaseManager
}
var NotificationManager *SNotificationManager
func init() {
NotificationManager = &SNotificationManager{
SStatusStandaloneResourceBaseManager: NewStatusStandaloneResourceBaseManager(
SNotification{},
"notify_t_notification",
"oldnotification",
"oldnotifications",
),
}
NotificationManager.SetVirtualObject(NotificationManager)
}
type SNotification struct {
SStatusStandaloneResourceBase
UID string `width:"128" nullable:"false" create:"required"`
ContactType string `width:"16" nullable:"false" create:"required" list:"user" index:"true"`
Topic string `width:"128" nullable:"true" create:"optional" list:"user"`
Priority string `width:"16" nullable:"true" create:"optional" list:"user"`
Msg string `create:"required"`
ReceivedAt time.Time `nullable:"true" list:"user" create:"optional"`
SendAt time.Time `nullable:"false"`
SendBy string `width:"128" nullable:"false"`
// ClusterID identify message with same topic, msg, priority
ClusterID string `width:"128" charset:"ascii" primary:"true" create:"optional" list:"user" get:"user"`
}
-51
View File
@@ -1,51 +0,0 @@
// 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 oldmodels
type STemplateManager struct {
SStandaloneResourceBaseManager
}
var TemplateManager *STemplateManager
func init() {
TemplateManager = &STemplateManager{
SStandaloneResourceBaseManager: NewStandaloneResourceBaseManager(
STemplate{},
"notify_t_template",
"oldtemplate",
"oldtemplates",
),
}
TemplateManager.SetVirtualObject(TemplateManager)
}
const (
TEMPLATE_TYPE_TITLE = "title"
TEMPLATE_TYPE_CONTENT = "content"
TEMPLATE_TYPE_REMOTE = "remote"
CONTACTTYPE_ALL = "all"
)
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" update:"user"`
}
-103
View File
@@ -1,103 +0,0 @@
// 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 oldmodels
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" 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)
}
-82
View File
@@ -1,82 +0,0 @@
// 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 oldmodels
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"
)
type SStatusStandaloneResourceBase struct {
SStandaloneResourceBase
Status string `width:"36" charset:"ascii" nullable:"false" default:"init" 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) 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) 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
}
-127
View File
@@ -1,127 +0,0 @@
// 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 oldmodels
import (
"context"
"fmt"
"yunion.io/x/sqlchemy"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/auth"
)
type SUserCacheManager struct {
db.SUserCacheManager
}
type SUser struct {
db.SUser
}
func (user *SUser) GetModelManager() db.IModelManager {
return UserCacheManager
}
var UserCacheManager *SUserCacheManager
func init() {
dbUserCacheManager := db.SUserCacheManager{
SKeystoneCacheObjectManager: db.NewKeystoneCacheObjectManager(
db.SUser{}, "users_cache_tbl", "olduser", "oldusers"),
}
UserCacheManager = &SUserCacheManager{
dbUserCacheManager,
}
UserCacheManager.SetVirtualObject(&dbUserCacheManager)
}
func RegistUserCredCacheUpdater() {
auth.RegisterAuthHook(onAuthCompleteUpdateCache)
}
func onAuthCompleteUpdateCache(ctx context.Context, userCred mcclient.TokenCredential) {
UserCacheManager.updateUserCache(ctx, userCred)
}
func (ucm *SUserCacheManager) updateUserCache(ctx context.Context, userCred mcclient.TokenCredential) {
ucm.Save(ctx, userCred.GetUserId(), userCred.GetUserName(),
userCred.GetDomainId(), userCred.GetDomainName())
}
func (ucm *SUserCacheManager) FetchUsersByIDs(ctx context.Context, ids []string) (map[string]SUser, error) {
q := ucm.Query().In("id", ids)
users := make([]db.SUser, 0)
err := db.FetchModelObjects(ucm, q, &users)
if err != nil {
return nil, err
}
ret := make(map[string]SUser)
for i := range users {
ret[users[i].Id] = SUser{users[i]}
}
// check that id is exist
for _, id := range ids {
if _, ok := ret[id]; ok {
continue
}
user, err := ucm.FetchUserFromKeystone(ctx, id)
if err != nil {
continue
}
ret[id] = SUser{*user}
}
return ret, nil
}
func (ucm *SUserCacheManager) FetchUserByIDOrName(ctx context.Context, idStr string) (*SUser, error) {
user, err := ucm.SUserCacheManager.FetchUserByIdOrName(ctx, idStr)
if err != nil {
return nil, err
}
return &SUser{*user}, nil
}
func (ucm *SUserCacheManager) FetchUserLikeName(ctx context.Context, name string, noExpireCheck bool) ([]SUser,
error) {
if !noExpireCheck {
// todo
return nil, fmt.Errorf("FetchUserLikeName with check Not Implement")
}
q := ucm.Query().Contains("name", name)
return ucm.FetchUserFromLoaclCache(ctx, q)
}
func (ucm *SUserCacheManager) FetchUserFromLoaclCache(ctx context.Context, q *sqlchemy.SQuery) ([]SUser, error) {
dbUsers := make([]db.SUser, 0, 1)
err := db.FetchModelObjects(ucm, q, &dbUsers)
if err != nil {
return nil, err
}
users := make([]SUser, len(dbUsers))
for i := range dbUsers {
users[i] = SUser{dbUsers[i]}
}
return users, nil
}
func (u *SUser) Delete(ctx context.Context, userCred mcclient.TokenCredential) error {
return u.SUser.Delete(ctx, userCred)
}
+3 -1
View File
@@ -22,6 +22,7 @@ type NotifyOption struct {
common_options.CommonOptions
common_options.DBOptions
DebugRequest bool `help:"Debug Send request"`
SocketFileDir string `help:"Socket file directory" default:"/etc/yunion/socket"`
UpdateInterval int `help:"Update send services interval(unit:min)" default:"30"`
@@ -34,7 +35,8 @@ type NotifyOption struct {
VerifyExpireInterval int `help:"expire interval of verify message; minutes" default:"2"`
VerifyValidInterval int `help:"valid interval of verify message; miniutes" default:"20"`
SyncReceiverIntervalMinutes int `help:"interval to sync receivers from keystone, in minutes" default:"30"`
SyncReceiverIntervalMinutes int `help:"interval to sync receivers from keystone, in minutes" default:"30"`
EnableWatchUser bool `help:"use etcd to watch user" default:"false"`
}
var Options NotifyOption
-90
View File
@@ -1,90 +0,0 @@
// 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 apis
import (
"context"
"time"
"google.golang.org/grpc"
)
type SendNotificationClient struct {
sendAgentClient
Conn *grpc.ClientConn
CallTimeout time.Duration
}
func NewSendNotificationClient(cc *grpc.ClientConn) *SendNotificationClient {
return &SendNotificationClient{
sendAgentClient: sendAgentClient{cc},
Conn: cc,
CallTimeout: 30 * time.Second,
}
}
func (c *SendNotificationClient) Send(ctx context.Context, in *SendParams, opts ...grpc.CallOption) (*Empty, error) {
ctx, cancel := context.WithTimeout(ctx, c.CallTimeout)
defer cancel()
return c.sendAgentClient.Send(ctx, in, opts...)
}
func (c *SendNotificationClient) AddConfig(ctx context.Context, in *AddConfigInput, opts ...grpc.CallOption) (*Empty, error) {
ctx, cancel := context.WithTimeout(ctx, c.CallTimeout)
defer cancel()
return c.sendAgentClient.AddConfig(ctx, in, opts...)
}
func (c *SendNotificationClient) DeleteConfig(ctx context.Context, in *DeleteConfigInput, opts ...grpc.CallOption) (*Empty, error) {
ctx, cancel := context.WithTimeout(ctx, c.CallTimeout)
defer cancel()
return c.sendAgentClient.DeleteConfig(ctx, in, opts...)
}
func (c *SendNotificationClient) UpdateConfig(ctx context.Context, in *UpdateConfigInput, opts ...grpc.CallOption) (*Empty, error) {
ctx, cancel := context.WithTimeout(ctx, c.CallTimeout)
defer cancel()
return c.sendAgentClient.UpdateConfig(ctx, in, opts...)
}
func (c *SendNotificationClient) CompleteConfig(ctx context.Context, in *CompleteConfigInput, opts ...grpc.CallOption) (*Empty, error) {
ctx, cancel := context.WithTimeout(ctx, c.CallTimeout)
defer cancel()
return c.sendAgentClient.CompleteConfig(ctx, in, opts...)
}
func (c *SendNotificationClient) ValidateConfig(ctx context.Context, in *ValidateConfigInput, opts ...grpc.CallOption) (*ValidateConfigReply, error) {
ctx, cancel := context.WithTimeout(ctx, c.CallTimeout)
defer cancel()
return c.sendAgentClient.ValidateConfig(ctx, in, opts...)
}
func (c *SendNotificationClient) UseridByMobile(ctx context.Context, in *UseridByMobileParams, opts ...grpc.CallOption) (*UseridByMobileReply, error) {
ctx, cancel := context.WithTimeout(ctx, c.CallTimeout)
defer cancel()
return c.sendAgentClient.UseridByMobile(ctx, in, opts...)
}
func (c *SendNotificationClient) Ready(ctx context.Context, in *ReadyInput, opts ...grpc.CallOption) (*ReadyOutput, error) {
ctx, cancel := context.WithTimeout(ctx, c.CallTimeout)
defer cancel()
return c.sendAgentClient.Ready(ctx, in, opts...)
}
func (c *SendNotificationClient) BatchSend(ctx context.Context, in *BatchSendParams, opts ...grpc.CallOption) (*BatchSendReply, error) {
ctx, cancel := context.WithTimeout(ctx, c.CallTimeout)
defer cancel()
return c.sendAgentClient.BatchSend(ctx, in, opts...)
}
File diff suppressed because it is too large Load Diff
-108
View File
@@ -1,108 +0,0 @@
// 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 {
SReceiver Receiver = 1;
string Topic = 2;
string Title = 3;
string Message = 4;
string Priority = 5;
string RemoteTemplate = 6;
}
message ValidateConfigInput {
map<string, string> configs = 1;
}
message AddConfigInput {
map<string, string> configs = 1;
string domainId = 2;
}
message UpdateConfigInput {
map<string, string> configs = 1;
string domainId = 2;
}
message DeleteConfigInput {
string domainId = 1;
}
message CompleteConfigInput {
repeated AddConfigInput ConfigInput = 1;
}
message UseridByMobileParams {
string mobile = 1;
string domainId = 2;
}
message Empty {
}
message UseridByMobileReply {
string userid = 1;
}
message ValidateConfigReply {
bool isValid = 1;
string msg = 2;
}
message SReceiver {
string Contact = 1;
string DomainId = 2;
}
message BatchSendParams {
repeated SReceiver Receivers = 1;
string Title = 2;
string Message = 3;
string Priority = 4;
string RemoteTemplate = 5;
}
message FailedRecord {
SReceiver Receiver = 1;
string Reason = 2;
}
message BatchSendReply {
repeated FailedRecord FailedRecords = 1;
}
message ReadyInput {
repeated string DomainIds = 1;
}
message ReadyOutput {
bool Ok = 1;
}
service SendAgent {
rpc Ready(ReadyInput) returns (ReadyOutput);
rpc Send (SendParams) returns (Empty);
rpc AddConfig (AddConfigInput) returns (Empty);
rpc CompleteConfig (CompleteConfigInput) returns (Empty);
rpc UpdateConfig (UpdateConfigInput) returns (Empty);
rpc DeleteConfig (DeleteConfigInput) returns (Empty);
rpc ValidateConfig (ValidateConfigInput) returns (ValidateConfigReply);
rpc UseridByMobile (UseridByMobileParams) returns (UseridByMobileReply);
rpc BatchSend (BatchSendParams) returns (BatchSendReply);
}
-480
View File
@@ -1,480 +0,0 @@
// 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 rpc
import (
"context"
"fmt"
"io/ioutil"
"net"
"os"
"path/filepath"
"strings"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/mcclient"
notifyv2 "yunion.io/x/onecloud/pkg/notify"
"yunion.io/x/onecloud/pkg/notify/rpc/apis"
"yunion.io/x/onecloud/pkg/util/fileutils2"
)
const (
// ErrSendServiceNotFound means SRpcService's SendSerivces hasn't this Send Service.
ErrSendServiceNotFound = errors.Error("No such send service")
// ErrSendServiceNotInit = errors.Error("Send service hasn't been init")
)
// SRpcService provide rpc service about sending message for notify module and manage these services.
// SendServices storage all send service.
type SRpcService struct {
SendServices *ServiceMap
socketFileDir string
configStore notifyv2.IServiceConfigStore
templateStore notifyv2.ITemplateStore
}
// NewSRpcService create a SRpcService
func NewSRpcService(socketFileDir string, configStore notifyv2.IServiceConfigStore,
tempalteStore notifyv2.ITemplateStore) *SRpcService {
return &SRpcService{
SendServices: NewServiceMap(),
socketFileDir: socketFileDir,
configStore: configStore,
templateStore: tempalteStore,
}
}
// 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)
}
ctx := context.Background()
for _, file := range files {
filename := file.Name()
if !file.IsDir() && strings.Contains(filename, ".sock") {
serviceName := filename[:len(filename)-5]
self.startNewService(ctx, serviceName, true)
}
}
if self.SendServices.Len() == 0 {
log.Infof("No available send service.")
} else {
log.Infof("Total %d send service init successful", self.SendServices.Len())
}
return nil
}
// UpdateServices will detect the self.sockFileDir, add new service and
// delete disappeared one from self.SendServices.
func (self *SRpcService) UpdateServices(ctx context.Context, usreCred mcclient.TokenCredential, isStart bool) {
err := self.updateService(ctx)
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() {
f := func(client *apis.SendNotificationClient) {
client.Conn.Close()
}
self.SendServices.Map(f)
}
// Send call the corresponding rpc server to send messager.
func (self *SRpcService) Send(ctx context.Context, contactType string, args apis.SendParams) error {
// Stop sending that must fail early
if len(args.RemoteTemplate) == 0 && contactType == api.MOBILE {
return fmt.Errorf("empty remote template for mobile type notification")
}
var err error
f := func(service *apis.SendNotificationClient) (interface{}, error) {
log.Debugf("send one")
return service.Send(ctx, &args)
}
_, err = self.execute(ctx, f, contactType)
if err != nil {
s, ok := status.FromError(err)
if !ok {
return err
}
return errors.Error(s.Message())
}
return nil
}
func (self *SRpcService) BatchSend(ctx context.Context, contactType string, args apis.BatchSendParams) ([]*apis.FailedRecord, error) {
// Stop sending that must fail early
if len(args.RemoteTemplate) == 0 && contactType == api.MOBILE {
return nil, fmt.Errorf("empty remote template for mobile type notification")
}
ret := make([]*apis.FailedRecord, 0)
f := func(service *apis.SendNotificationClient) (interface{}, error) {
return service.BatchSend(ctx, &args)
}
i, err := self.execute(ctx, f, contactType)
if err != nil {
s, ok := status.FromError(err)
if !ok {
return nil, err
}
return nil, errors.Error(s.Message())
}
reply := i.(*apis.BatchSendReply)
return append(ret, reply.FailedRecords...), nil
}
// UpdateConfig can update config for rpc service with domainId
func (self *SRpcService) UpdateConfig(ctx context.Context, service string, config notifyv2.SConfig) error {
var (
sendService *apis.SendNotificationClient
err error
)
sendService, ok := self.SendServices.Get(service)
if !ok {
return fmt.Errorf("no such service %s", service)
}
args := apis.UpdateConfigInput{
Configs: config.Config,
DomainId: config.DomainId,
}
_, err = sendService.UpdateConfig(ctx, &args)
if err != nil {
st := status.Convert(err)
if st.Code() != codes.NotFound {
return errors.Error(st.Message())
}
_, err = sendService.AddConfig(ctx, &apis.AddConfigInput{
Configs: config.Config,
DomainId: config.DomainId,
})
if err != nil {
return errors.Wrap(err, "try to add config but failed")
}
}
return nil
}
func (self *SRpcService) ContactByMobile(ctx context.Context, mobile, serviceName string, domainId string) (string, error) {
iMobile := api.ParseInternationalMobile(mobile)
// compatible
if iMobile.AreaCode == "86" {
mobile = iMobile.Mobile
}
args := apis.UseridByMobileParams{
Mobile: mobile,
DomainId: domainId,
}
f := func(service *apis.SendNotificationClient) (interface{}, error) {
return service.UseridByMobile(ctx, &args)
}
ret, err := self.execute(ctx, f, serviceName)
if err == nil {
reply := ret.(*apis.UseridByMobileReply)
return reply.Userid, nil
}
s, ok := status.FromError(err)
if !ok {
return "", err
}
if s.Code() == codes.NotFound {
return "", errors.Wrap(notifyv2.ErrNoSuchMobile, s.Message())
}
if s.Code() == codes.FailedPrecondition {
return "", errors.Wrap(notifyv2.ErrIncompleteConfig, s.Message())
}
return "", err
}
// Wrap function to execute function call rpc server
func (self *SRpcService) execute(ctx context.Context, f func(client *apis.SendNotificationClient) (interface{}, error),
serviceName string) (interface{}, error) {
sendService, ok := self.SendServices.Get(serviceName)
log.Debugf("get service %s", serviceName)
var err error
if !ok {
log.Debugf("get service first time failed")
sendService, err = self.startNewService(ctx, serviceName, true)
if err != nil {
return nil, errors.Wrap(err, "start new service failed")
}
}
ret, err := f(sendService)
if err != nil {
// hander error
st := status.Convert(err)
if st.Code() == codes.Unavailable {
// sock is bad
self.closeService(ctx, serviceName)
return nil, ErrSendServiceNotFound
}
return nil, err
}
return ret, nil
}
var ErrGetConfig = errors.Error("Get Config Failed")
func (self *SRpcService) completeConfig(ctx context.Context, serviceName string, sendService *apis.SendNotificationClient) error {
// get config
configs, err := self.configStore.GetConfigs(serviceName)
if err != nil {
log.Errorf("getConfig of serveice %s from database error", serviceName)
return ErrGetConfig
}
// update config for service
configInput := make([]*apis.AddConfigInput, len(configs))
for i := range configInput {
configInput[i] = &apis.AddConfigInput{
Configs: configs[i].Config,
DomainId: configs[i].DomainId,
}
}
_, err = sendService.CompleteConfig(ctx, &apis.CompleteConfigInput{
ConfigInput: configInput,
})
if err != nil {
st := status.Convert(err)
if st.Code() == codes.FailedPrecondition {
// no such rpc serve
err = fmt.Errorf(st.Message())
}
if st.Code() == codes.Unavailable {
err = fmt.Errorf("service is unavailable for now: %s", st.Message())
}
return errors.Wrap(err, "UpdateConfig")
}
return nil
}
func (self *SRpcService) restartService(ctx context.Context, service string) (*apis.SendNotificationClient, error) {
sendService, ok := self.SendServices.Get(service)
if !ok {
return nil, fmt.Errorf("no such service, please start new service")
}
return sendService, self.completeConfig(ctx, service, sendService)
}
// startNewService try to start a new rpc service named serviceName
// passConfig means if pass config to send service
func (self *SRpcService) startNewService(ctx context.Context, serviceName string, passConfig bool) (*apis.SendNotificationClient, error) {
var (
sendService *apis.SendNotificationClient
err error
)
filename := filepath.Join(self.socketFileDir, serviceName+".sock")
if !fileutils2.Exists(filename) {
return nil, errors.Error(fmt.Sprintf("no such socket file '%s'", filename))
}
grpcConn, err := grpcDialWithUnixSocket(ctx, filename)
if err != nil {
return nil, err
}
sendService = apis.NewSendNotificationClient(grpcConn)
self.SendServices.Set(sendService, serviceName)
if !passConfig {
return sendService, nil
}
return sendService, self.completeConfig(ctx, serviceName, sendService)
}
// closeService will remove service record from self.SendServices and try to remove sock file
func (self *SRpcService) closeService(ctx context.Context, serviceName string) {
filename := filepath.Join(self.socketFileDir, serviceName+".sock")
self.SendServices.Remove(serviceName)
os.Remove(filename)
}
func (self *SRpcService) updateService(ctx context.Context) error {
files, err := ioutil.ReadDir(self.socketFileDir)
if err != nil {
return errors.Wrapf(err, "read dir %s failed", self.socketFileDir)
}
serviceNames := self.SendServices.ServiceNames()
serviceNameSet := make(map[string]struct{})
for _, name := range serviceNames {
serviceNameSet[name] = struct{}{}
}
for _, file := range files {
filename := file.Name()
if !file.IsDir() && strings.Contains(filename, ".sock") {
serviceName := filename[:len(filename)-5]
if self.SendServices.IsExist(serviceName) {
delete(serviceNameSet, serviceName)
continue
}
self.startNewService(ctx, serviceName, true)
}
}
serviceNames = serviceNames[:0]
for serviceName := range serviceNameSet {
serviceNames = append(serviceNames, serviceName)
}
self.SendServices.BatchRemove(serviceNames)
return nil
}
func (self *SRpcService) AddConfig(ctx context.Context, service string, config notifyv2.SConfig) error {
var (
sendService *apis.SendNotificationClient
err error
)
sendService, ok := self.SendServices.Get(service)
if !ok {
return fmt.Errorf("no such service %s", service)
}
args := apis.AddConfigInput{
DomainId: config.DomainId,
Configs: config.Config,
}
_, err = sendService.AddConfig(ctx, &args)
if err != nil {
return err
}
return nil
}
func (self *SRpcService) DeleteConfig(ctx context.Context, service, domainId string) error {
var (
sendService *apis.SendNotificationClient
err error
)
sendService, ok := self.SendServices.Get(service)
if !ok {
return fmt.Errorf("no such service %s", service)
}
args := apis.DeleteConfigInput{
DomainId: domainId,
}
_, err = sendService.DeleteConfig(ctx, &args)
if err != nil {
return err
}
return nil
}
func (self *SRpcService) ValidateConfig(ctx context.Context, cType string, configs map[string]string) (isValid bool, message string, err error) {
sendService, ok := self.SendServices.Get(cType)
log.Debugf("get service %s", cType)
if !ok {
log.Debugf("get service first time failed")
sendService, err = self.startNewService(ctx, cType, false)
if err != nil {
err = errors.Wrap(err, "start new service failed")
return
}
}
param := apis.ValidateConfigInput{
Configs: configs,
}
rep, err := sendService.ValidateConfig(ctx, &param)
if err != nil {
st := status.Convert(err)
if st.Code() == codes.Unimplemented {
err = errors.ErrNotImplemented
return
}
err = fmt.Errorf(st.Message())
return
}
return rep.IsValid, rep.Msg, nil
}
func robotType2ContactType(rType string) string {
switch rType {
case api.ROBOT_TYPE_FEISHU:
return api.FEISHU_ROBOT
case api.ROBOT_TYPE_DINGTALK:
return api.DINGTALK_ROBOT
case api.ROBOT_TYPE_WORKWX:
return api.WORKWX_ROBOT
case api.ROBOT_TYPE_WEBHOOK:
return api.WEBHOOK
}
return rType
}
func (self *SRpcService) SendRobotMessage(ctx context.Context, rType string, receivers []*apis.SReceiver, title string, message string) ([]*apis.FailedRecord, error) {
log.Infof("rType: %s", rType)
contactType := robotType2ContactType(rType)
args := apis.BatchSendParams{
Receivers: receivers,
Title: title,
Message: message,
}
f := func(service *apis.SendNotificationClient) (interface{}, error) {
return service.BatchSend(ctx, &args)
}
ret, err := self.execute(ctx, f, contactType)
if err != nil {
s, ok := status.FromError(err)
if !ok {
return nil, err
}
return nil, errors.Error(s.Message())
}
reply := ret.(*apis.BatchSendReply)
return reply.FailedRecords, nil
}
func grpcDialWithUnixSocket(ctx context.Context, socketPath string) (*grpc.ClientConn, error) {
return grpc.DialContext(ctx, socketPath, grpc.WithInsecure(), grpc.WithTimeout(time.Second*5), grpc.WithDialer(
func(addr string, timeout time.Duration) (net.Conn, error) {
return net.DialTimeout("unix", addr, timeout)
}),
)
}
-97
View File
@@ -1,97 +0,0 @@
// 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 rpc
import (
"sync"
"yunion.io/x/onecloud/pkg/notify/rpc/apis"
)
// ServiceMap has a map of string and apis.SendNotification's pointer, and a RWMutex lock protect map.
type ServiceMap struct {
serviceMap map[string]*apis.SendNotificationClient
lock sync.RWMutex
}
func NewServiceMap() *ServiceMap {
return &ServiceMap{serviceMap: make(map[string]*apis.SendNotificationClient)}
}
func (sm *ServiceMap) Get(serviceName string) (*apis.SendNotificationClient, bool) {
sm.lock.RLock()
defer sm.lock.RUnlock()
client, ok := sm.serviceMap[serviceName]
return client, ok
}
func (sm *ServiceMap) Set(service *apis.SendNotificationClient, serviceName string) {
sm.lock.Lock()
defer sm.lock.Unlock()
sm.serviceMap[serviceName] = service
}
func (sm *ServiceMap) Remove(serviceName string) {
sm.lock.Lock()
defer sm.lock.Unlock()
service, ok := sm.serviceMap[serviceName]
if !ok {
return
}
service.Conn.Close()
delete(sm.serviceMap, serviceName)
}
func (sm *ServiceMap) BatchRemove(serviceNames []string) {
sm.lock.Lock()
defer sm.lock.Unlock()
for _, serviceName := range serviceNames {
service, ok := sm.serviceMap[serviceName]
if !ok {
continue
}
service.Conn.Close()
delete(sm.serviceMap, serviceName)
}
}
func (sm *ServiceMap) ServiceNames() []string {
sm.lock.RLock()
defer sm.lock.RUnlock()
serviceNames := make([]string, 0, len(sm.serviceMap))
for serviceName := range sm.serviceMap {
serviceNames = append(serviceNames, serviceName)
}
return serviceNames
}
func (sm *ServiceMap) IsExist(serviceName string) bool {
sm.lock.RLock()
defer sm.lock.RUnlock()
_, ok := sm.serviceMap[serviceName]
return ok
}
func (sm *ServiceMap) Len() int {
return len(sm.serviceMap)
}
func (sm *ServiceMap) Map(f func(*apis.SendNotificationClient)) {
sm.lock.Lock()
defer sm.lock.Unlock()
for _, service := range sm.serviceMap {
f(service)
}
}
+53
View File
@@ -0,0 +1,53 @@
// 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 sender
import (
"yunion.io/x/pkg/errors"
)
var (
ErrIPWhiteList = errors.Error("Need to add ip to whtelist")
ErrNoSupportSecSetting = errors.Error("Only the IP address option in the security Settings is supported")
ErrNoSuchMobile = errors.Error("No such mobile")
ErrIncompleteConfig = errors.Error("Incomplete config")
ErrDuplicateConfig = errors.Error("Duplicate config for a domain")
)
const (
ApiWebhookRobotV2SendMessage = "https://open.feishu.cn/open-apis/bot/v2/hook/"
// 钉钉发送消息
ApiDingtalkSendMessage = "https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2?"
// 钉钉获取token
ApiDingtalkGetToken = "https://oapi.dingtalk.com/gettoken?"
// 钉钉使用手机号获取用户ID
ApiDingtalkGetUserByMobile = "https://oapi.dingtalk.com/topapi/v2/user/getbymobile?"
// 钉钉获取消息发送结果
ApiDingtalkGetSendResult = "https://oapi.dingtalk.com/topapi/message/corpconversation/getsendresult?"
// 企业微信获取token
ApiWorkwxGetToken = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?"
// 企业微信使用手机号获取用户ID
ApiWorkwxGetUserByMobile = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserid?"
// 企业微信发送消息
ApiWorkwxSendMessage = "https://qyapi.weixin.qq.com/cgi-bin/message/send?"
// 飞书使用手机号或邮箱获取用户ID
ApiFetchUserID = "https://open.feishu.cn/open-apis/user/v1/batch_get_id?"
)
var (
FAIL_KEY = []string{"失败", "fail ", "failed"}
)
const EVENT_HEADER = "X-Yunion-Event"
+162
View File
@@ -0,0 +1,162 @@
// 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 sender
import (
"fmt"
"net/url"
"strings"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/httputils"
api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/notify/models"
)
type SDingTalkSender struct {
config map[string]api.SNotifyConfigContent
}
func (dingSender *SDingTalkSender) GetSenderType() string {
return api.DINGTALK
}
func (dingSender *SDingTalkSender) Send(args api.SendParams) error {
body := map[string]interface{}{
"agent_id": models.ConfigMap[api.DINGTALK].Content.AgentId,
"msg": map[string]interface{}{
"msgtype": "markdown",
"markdown": map[string]interface{}{
"title": args.Title,
"text": fmt.Sprintf("%s (%s)", args.Message, time.Now().Format("2006-01-02 15:04")),
},
},
"userid_list": args.Receivers.Contact,
}
params := url.Values{}
params.Set("access_token", models.ConfigMap[api.DINGTALK].Content.AccessToken)
req, err := sendRequest(ApiDingtalkSendMessage, httputils.POST, nil, params, jsonutils.Marshal(body))
if err != nil {
subCode, _ := req.GetString("sub_code")
switch subCode {
// token失效或不合法
case "40014":
// 尝试重新获取token
err = dingSender.GetAccessToken()
if err != nil {
return errors.Wrap(err, "reset token")
}
// 重新发送通知
params = url.Values{}
params.Set("access_token", models.ConfigMap[api.DINGTALK].Content.AccessToken)
req, err = sendRequest(ApiDingtalkSendMessage, httputils.POST, nil, params, jsonutils.Marshal(body))
if err != nil {
return errors.Wrap(err, "dingtalk resend message")
}
}
if err != nil {
return errors.Wrap(err, "dingtalk send message")
}
}
// 获取消息通知发送结果
task_id, _ := req.GetString("task_id")
body = map[string]interface{}{
"agent_id": models.ConfigMap[api.DINGTALK].Content.AgentId,
"task_id": task_id,
}
_, err = sendRequest(ApiDingtalkSendMessage, httputils.POST, nil, params, jsonutils.Marshal(body))
return err
}
func (dingSender *SDingTalkSender) ValidateConfig(config api.NotifyConfig) (string, error) {
// 校验accesstoken
_, err := dingSender.getAccessToken(config.AppKey, config.AppSecret)
if err != nil {
if strings.Contains(err.Error(), "40089") {
return "invalid AppKey or AppSecret", nil
}
return "", err
}
models.ConfigMap[api.DINGTALK].Content.AppKey, models.ConfigMap[api.DINGTALK].Content.AppSecret = config.AppKey, config.AppSecret
return "", nil
}
func (dingSender *SDingTalkSender) ContactByMobile(mobile, domainId string) (string, error) {
err := dingSender.GetAccessToken()
if err != nil {
return "", err
}
body := jsonutils.Marshal(map[string]interface{}{
"mobile": mobile,
})
params := url.Values{}
params.Set("access_token", models.ConfigMap[api.DINGTALK].Content.AccessToken)
res, err := sendRequest(ApiDingtalkGetUserByMobile, httputils.POST, nil, params, body)
if err != nil {
return "", errors.Wrap(err, "get user by mobile")
}
return res.GetString("result", "userid")
}
func (dingSender *SDingTalkSender) IsPersonal() bool {
return true
}
func (dingSender *SDingTalkSender) IsRobot() bool {
return false
}
func (dingSender *SDingTalkSender) IsValid() bool {
return len(dingSender.config) > 0
}
func (dingSender *SDingTalkSender) IsPullType() bool {
return true
}
func (dingSender *SDingTalkSender) IsSystemConfigContactType() bool {
return true
}
func (dingSender *SDingTalkSender) GetAccessToken() error {
appKey, appSecret := models.ConfigMap[api.DINGTALK].Content.AppKey, models.ConfigMap[api.DINGTALK].Content.AppSecret
token, err := dingSender.getAccessToken(appKey, appSecret)
if err != nil {
return errors.Wrap(err, "dingtalk getAccessToken")
}
models.ConfigMap[api.DINGTALK].Content.AccessToken = token
return nil
}
func (dingSender *SDingTalkSender) getAccessToken(appKey, appSecret string) (string, error) {
params := url.Values{}
params.Set("appkey", appKey)
params.Set("appsecret", appSecret)
res, err := sendRequest(ApiDingtalkGetToken, httputils.GET, nil, params, nil)
if err != nil {
return "", errors.Wrap(err, "get dingtalk token")
}
return res.GetString("access_token")
}
func init() {
models.Register(&SDingTalkSender{
config: map[string]api.SNotifyConfigContent{},
})
}
+110
View File
@@ -0,0 +1,110 @@
// 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 sender
import (
"fmt"
"strings"
"github.com/hugozhu/godingtalk"
"yunion.io/x/cloudmux/pkg/cloudprovider"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/errors"
api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/notify/models"
)
const (
WEBHOOK_PREFIX = "https://oapi.dingtalk.com/robot/send?access_token="
)
type SDingTalkRobotSender struct {
config map[string]api.SNotifyConfigContent
}
func (dingRobotSender *SDingTalkRobotSender) GetSenderType() string {
return api.DINGTALK_ROBOT
}
func (dingRobotSender *SDingTalkRobotSender) Send(args api.SendParams) error {
var token string
var atStr strings.Builder
title, msg := args.Title, args.Message
webhook := args.Receivers.Contact
if strings.HasPrefix(webhook, WEBHOOK_PREFIX) {
token = webhook[len(WEBHOOK_PREFIX):]
} else {
return errors.Wrap(InvalidWebhook, webhook)
}
processText := fmt.Sprintf("### %s\n%s%s", title, msg, atStr.String())
// atList := &godingtalk.RobotAtList{}
client := godingtalk.NewDingTalkClient("", "")
rep, err := client.SendRobotMarkdownMessage(token, title, processText)
if err == nil {
return nil
}
if rep.ErrCode == 310000 {
if strings.Contains(rep.ErrMsg, "whitelist") {
return errors.Wrap(ErrIPWhiteList, rep.ErrMsg)
} else {
return errors.Wrap(err, jsonutils.Marshal(rep).PrettyString())
}
}
if rep.ErrCode == 300001 && strings.Contains(rep.ErrMsg, "token") {
return ErrNoSuchWebhook
}
return errors.Wrap(err, "this is res err")
}
func (dingRobotSender *SDingTalkRobotSender) ValidateConfig(config api.NotifyConfig) (string, error) {
return "", cloudprovider.ErrNotImplemented
}
func (dingRobotSender *SDingTalkRobotSender) ContactByMobile(mobile, domainId string) (string, error) {
return "", cloudprovider.ErrNotImplemented
}
func (dingRobotSender *SDingTalkRobotSender) IsPersonal() bool {
return true
}
func (dingRobotSender *SDingTalkRobotSender) IsRobot() bool {
return true
}
func (dingRobotSender *SDingTalkRobotSender) IsValid() bool {
return len(dingRobotSender.config) > 0
}
func (dingRobotSender *SDingTalkRobotSender) IsPullType() bool {
return true
}
func (dingRobotSender *SDingTalkRobotSender) IsSystemConfigContactType() bool {
return true
}
func (dingRobotSender *SDingTalkRobotSender) GetAccessToken() error {
return nil
}
func init() {
models.Register(&SDingTalkRobotSender{
config: map[string]api.SNotifyConfigContent{},
})
}
+191 -8
View File
@@ -18,15 +18,18 @@ import (
"crypto/tls"
"encoding/base64"
"io"
"net/http"
"net/url"
"time"
"gopkg.in/mail.v2"
gomail "gopkg.in/mail.v2"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/httputils"
api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/notify/models"
)
type errorMap map[string]error
@@ -39,8 +42,189 @@ func (em errorMap) Error() string {
return jsonutils.Marshal(msg).String()
}
type SEmailSender struct {
config map[string]api.SNotifyConfigContent
}
func (emailSender *SEmailSender) GetSenderType() string {
return api.EMAIL
}
func (emailSender *SEmailSender) Send(args api.SendParams) error {
// 初始化emaliClient
hostNmae, hostPort, userName, password := models.ConfigMap[api.EMAIL].Content.Hostname, models.ConfigMap[api.EMAIL].Content.Hostport, models.ConfigMap[api.EMAIL].Content.Username, models.ConfigMap[api.EMAIL].Content.Password
dialer := gomail.NewDialer(hostNmae, hostPort, userName, password)
// 是否支持ssl
if models.ConfigMap[api.EMAIL].Content.SslGlobal {
dialer.SSL = true
} else {
dialer.SSL = false
dialer.TLSConfig = &tls.Config{
InsecureSkipVerify: true,
}
}
if len(args.EmailMsg.To) > 0 {
// emailMsg不为空时
sender, err := dialer.Dial()
if err != nil {
return errors.Wrap(err, "dialer.Dial")
}
retErr := errorMap{}
for _, to := range args.EmailMsg.To {
gmsg := gomail.NewMessage()
gmsg.SetHeader("From", models.ConfigMap[api.EMAIL].Content.SenderAddress)
gmsg.SetHeader("To", to)
gmsg.SetHeader("Subject", args.EmailMsg.Subject)
gmsg.SetBody("text/html", args.EmailMsg.Body)
for i := range args.EmailMsg.Attachments {
attach := args.EmailMsg.Attachments[i]
gmsg.Attach(attach.Filename,
gomail.SetCopyFunc(func(w io.Writer) error {
mime := attach.Mime
if len(mime) == 0 {
mime = "application/octet-stream"
}
_, err := w.Write([]byte("Content-Type: " + attach.Mime))
return errors.Wrap(err, "WriteMime")
}),
gomail.SetCopyFunc(func(w io.Writer) error {
contBytes, err := base64.StdEncoding.DecodeString(attach.Base64Content)
if err != nil {
return errors.Wrap(err, "base64.StdEncoding.DecodeString")
}
_, err = w.Write(contBytes)
return errors.Wrap(err, "WriteContent")
}),
)
errs := make([]error, 0)
for tryTime := 3; tryTime > 0; tryTime-- {
err = gomail.Send(sender, gmsg)
if err != nil {
errs = append(errs, err)
time.Sleep(time.Second * 10)
continue
}
errs = errs[0:0]
break
}
if len(errs) > 0 {
retErr[to] = errors.NewAggregate(errs)
}
}
}
}
// 构造email发送请求
gmsg := gomail.NewMessage()
gmsg.SetHeader("From", models.ConfigMap[api.EMAIL].Content.SenderAddress)
gmsg.SetHeader("To", args.Receivers.Contact)
gmsg.SetHeader("Subject", args.Title)
gmsg.SetBody("text/html", args.Message)
dialer.StartTLSPolicy = gomail.MandatoryStartTLS
if err := dialer.DialAndSend(gmsg); err != nil {
return errors.Wrap(err, "send email")
}
return nil
}
func (emailSender *SEmailSender) ValidateConfig(config api.NotifyConfig) (string, error) {
errChan := make(chan error, 1)
go func() {
dialer := gomail.NewDialer(config.Hostname, config.Hostport, config.Username, config.Password)
if config.SslGlobal {
dialer.SSL = true
} else {
dialer.SSL = false
// StartLSConfig
dialer.TLSConfig = &tls.Config{
InsecureSkipVerify: true,
}
}
sender, err := dialer.Dial()
if err != nil {
errChan <- err
return
}
sender.Close()
errChan <- nil
}()
ticker := time.Tick(10 * time.Second)
select {
case <-ticker:
return "", errors.Error("timeout")
case err := <-errChan:
return "", err
}
}
func (emailSender *SEmailSender) ContactByMobile(mobile, domainId string) (string, error) {
return "", nil
}
func (emailSender *SEmailSender) IsPersonal() bool {
return true
}
func (emailSender *SEmailSender) IsRobot() bool {
return false
}
func (emailSender *SEmailSender) IsValid() bool {
return len(emailSender.config) > 0
}
func (emailSender *SEmailSender) IsPullType() bool {
return false
}
func (emailSender *SEmailSender) IsSystemConfigContactType() bool {
return true
}
func (emailSender *SEmailSender) GetAccessToken() error {
corpId, secret := models.ConfigMap[api.WORKWX].Content.CorpId, models.ConfigMap[api.WORKWX].Content.Secret
token, err := emailSender.getAccessToken(corpId, secret)
if err != nil {
return errors.Wrap(err, "workwx getAccessToken")
}
models.ConfigMap[api.WORKWX].Content.AccessToken = token
return nil
}
func (emailSender *SEmailSender) getAccessToken(corpId, secret string) (string, error) {
// url := ApiWorkwxGetToken + fmt.Sprintf("?corpid=%s&corpsecret=%s", corpId, secret)
params := url.Values{}
params.Set("corpid", corpId)
params.Set("corpsecret", secret)
res, err := sendRequest(ApiWorkwxGetToken, httputils.GET, nil, params, nil)
if err != nil {
return "", errors.Wrap(err, "get workwx token")
}
return res.GetString("access_token")
}
func (emailSender *SEmailSender) sendMessageWithToken(uri string, method httputils.THttpMethod, header http.Header, params url.Values, body jsonutils.JSONObject) (jsonutils.JSONObject, error) {
if params == nil {
params = url.Values{}
}
params.Set("access_token", models.ConfigMap[api.WORKWX].Content.AccessToken)
return sendRequest(uri, httputils.POST, nil, params, jsonutils.Marshal(body))
}
func init() {
models.Register(&SEmailSender{
config: map[string]api.SNotifyConfigContent{},
})
}
func SendEmail(conf *api.SEmailConfig, msg *api.SEmailMessage) error {
dialer := mail.NewDialer(conf.Hostname, conf.Hostport, conf.Username, conf.Password)
dialer := gomail.NewDialer(conf.Hostname, conf.Hostport, conf.Username, conf.Password)
if conf.SslGlobal {
dialer.SSL = true
@@ -59,7 +243,7 @@ func SendEmail(conf *api.SEmailConfig, msg *api.SEmailMessage) error {
retErr := errorMap{}
for _, to := range msg.To {
gmsg := mail.NewMessage()
gmsg := gomail.NewMessage()
gmsg.SetHeader("From", conf.SenderAddress)
gmsg.SetHeader("To", to)
gmsg.SetHeader("Subject", msg.Subject)
@@ -68,7 +252,7 @@ func SendEmail(conf *api.SEmailConfig, msg *api.SEmailMessage) error {
for i := range msg.Attachments {
attach := msg.Attachments[i]
gmsg.Attach(attach.Filename,
mail.SetCopyFunc(func(w io.Writer) error {
gomail.SetCopyFunc(func(w io.Writer) error {
mime := attach.Mime
if len(mime) == 0 {
mime = "application/octet-stream"
@@ -76,7 +260,7 @@ func SendEmail(conf *api.SEmailConfig, msg *api.SEmailMessage) error {
_, err := w.Write([]byte("Content-Type: " + attach.Mime))
return errors.Wrap(err, "WriteMime")
}),
mail.SetCopyFunc(func(w io.Writer) error {
gomail.SetCopyFunc(func(w io.Writer) error {
contBytes, err := base64.StdEncoding.DecodeString(attach.Base64Content)
if err != nil {
return errors.Wrap(err, "base64.StdEncoding.DecodeString")
@@ -89,8 +273,7 @@ func SendEmail(conf *api.SEmailConfig, msg *api.SEmailMessage) error {
errs := make([]error, 0)
for tryTime := 3; tryTime > 0; tryTime-- {
err = mail.Send(sender, gmsg)
log.Debugf("send email ...")
err = gomail.Send(sender, gmsg)
if err != nil {
errs = append(errs, err)
time.Sleep(time.Second * 10)
+163
View File
@@ -0,0 +1,163 @@
// 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 sender
import (
"fmt"
"net/http"
"net/url"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/httputils"
api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/monitor/notifydrivers/feishu"
"yunion.io/x/onecloud/pkg/notify/models"
)
type SFeishuSender struct {
config map[string]api.SNotifyConfigContent
}
func (self *SFeishuSender) GetSenderType() string {
return api.FEISHU
}
const ApiSendMessageForFeishuByOpenId = feishu.ApiRobotSendMessage + "?receive_id_type=open_id"
// 发送飞书消息
func (feishuSender *SFeishuSender) Send(args api.SendParams) error {
// 发送通知消息体
body := map[string]interface{}{
"open_id": args.Receivers.Contact,
"msg_type": "text",
"content": map[string]interface{}{
"text": args.Message,
},
} // 添加bearer token请求头
header := http.Header{}
header.Add("Authorization", fmt.Sprintf("Bearer %s", models.ConfigMap[api.FEISHU].Content.AccessToken))
req, err := sendRequest(ApiSendMessageForFeishuByOpenId, httputils.POST, header, nil, jsonutils.Marshal(body))
if err == nil {
return nil
}
// 发送通知失败的情况
code, _ := req.GetString("code")
switch code {
case "99991663": //token过期
err = feishuSender.GetAccessToken()
if err != nil {
return errors.Wrap(err, "tenant token invalid && getToken err")
}
header.Set("Authorization", fmt.Sprintf("Bearer %s", models.ConfigMap[api.FEISHU].Content.AccessToken))
_, err = sendRequest(ApiSendMessageForFeishuByOpenId, httputils.POST, header, nil, jsonutils.Marshal(body))
if err == nil {
return nil
}
case "99991672": // 未开放发送消息通知的权限
err = errors.Wrap(ErrIncompleteConfig, err.Error())
}
return err
}
// 校验appId与appSecret
func (feishuSender *SFeishuSender) ValidateConfig(config api.NotifyConfig) (string, error) {
rep, err := feishuSender.getAccessToken(config.AppId, config.AppSecret)
if err == nil {
return "", err
}
var msg string
switch rep.Code {
case 10003:
msg = "invalid AppId"
case 10014:
msg = "invalid AppSecret"
}
return msg, nil
}
// 根据用户手机号获取用户的open_id
func (feishuSender *SFeishuSender) ContactByMobile(mobile, domainId string) (string, error) {
body := jsonutils.NewDict()
body.Set("mobiles", jsonutils.NewArray(jsonutils.NewString(mobile)))
header := http.Header{}
// 考虑到获取用户id需求较少,可通过直接更新token来避免token失效
err := feishuSender.GetAccessToken()
if err != nil {
return "", errors.Wrap(err, "GetAccessToken")
}
params := url.Values{}
params.Set("mobiles", mobile)
header.Set("Authorization", fmt.Sprintf("Bearer %s", models.ConfigMap[api.FEISHU].Content.AccessToken))
resp, err := sendRequest(ApiFetchUserID, httputils.GET, header, params, body)
if err != nil {
return "", err
}
mobileNotExist, _ := resp.GetArray("data", "mobiles_not_exist")
if len(mobileNotExist) != 0 {
return "", errors.Wrapf(errors.ErrNotFound, "no such user whose mobile is %s", mobile)
}
list, err := resp.GetArray("data", "mobile_users", mobile)
if err != nil {
return "", errors.Wrap(err, "jsonutils.JSONObject.GetArray")
}
// len(list) must be positive
return list[0].GetString("open_id")
}
func (feishuSender *SFeishuSender) IsPersonal() bool {
return true
}
func (feishuSender *SFeishuSender) IsRobot() bool {
return false
}
func (feishuSender *SFeishuSender) IsValid() bool {
return len(feishuSender.config) > 0
}
func (feishuSender *SFeishuSender) IsPullType() bool {
return true
}
func (feishuSender *SFeishuSender) IsSystemConfigContactType() bool {
return true
}
// 获取token
func (feishuSender *SFeishuSender) GetAccessToken() error {
appId, appSecret := models.ConfigMap[api.FEISHU].Content.AppId, models.ConfigMap[api.FEISHU].Content.AppSecret
resp, err := feishuSender.getAccessToken(appId, appSecret)
models.ConfigMap[api.FEISHU].Content.AccessToken = resp.TenantAccessToken
return err
}
func (feishuSender *SFeishuSender) getAccessToken(appId, appSecret string) (*feishu.TenantAccesstokenResp, error) {
resp, err := feishu.GetTenantAccessTokenInternal(appId, appSecret)
if err != nil {
return resp, errors.Wrap(err, "feishu.GetTenantAccessTokenInternal")
}
return resp, nil
}
func init() {
models.Register(&SFeishuSender{
config: map[string]api.SNotifyConfigContent{},
})
}
+112
View File
@@ -0,0 +1,112 @@
// 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 sender
import (
"fmt"
"strings"
"yunion.io/x/cloudmux/pkg/cloudprovider"
"yunion.io/x/pkg/errors"
api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/monitor/notifydrivers/feishu"
"yunion.io/x/onecloud/pkg/notify/models"
)
var ErrNoSuchWebhook = errors.Error("No such webhook")
var InvalidWebhook = errors.Error("Invalid webhook")
type SFeishuRobotSender struct {
config map[string]api.SNotifyConfigContent
}
func (feishuRobotSender *SFeishuRobotSender) GetSenderType() string {
return api.FEISHU_ROBOT
}
func (feishuRobotSender *SFeishuRobotSender) Send(args api.SendParams) error {
var token string
var errs []error
title, msg := args.Title, args.Message
webhook := args.Receivers.Contact
switch {
case strings.HasPrefix(webhook, ApiWebhookRobotV2SendMessage):
token = webhook[len(ApiWebhookRobotV2SendMessage):]
case strings.HasPrefix(webhook, feishu.ApiWebhookRobotSendMessage):
token = webhook[len(feishu.ApiWebhookRobotSendMessage):]
default:
return errors.Wrap(InvalidWebhook, webhook)
}
req := feishu.WebhookRobotMsgReq{
Title: title,
Text: msg,
}
rep, err := feishu.SendWebhookRobotMessage(token, req)
if err != nil {
return errors.Wrap(err, "SendWebhookRobotMessage")
}
if !rep.Ok {
if strings.Contains(rep.Error, "token") {
return ErrNoSuchWebhook
} else {
return fmt.Errorf("SendWebhookRobotMessage failed: %s", rep.Error)
}
}
if err != nil {
if errs == nil {
errs = []error{}
}
errs = append(errs, err)
}
return errors.NewAggregate(errs)
}
func (feishuRobotSender *SFeishuRobotSender) ValidateConfig(config api.NotifyConfig) (string, error) {
return "", cloudprovider.ErrNotImplemented
}
func (feishuRobotSender *SFeishuRobotSender) ContactByMobile(mobile, domainId string) (string, error) {
return "", cloudprovider.ErrNotImplemented
}
func (feishuRobotSender *SFeishuRobotSender) IsPersonal() bool {
return true
}
func (feishuRobotSender *SFeishuRobotSender) IsRobot() bool {
return true
}
func (feishuRobotSender *SFeishuRobotSender) IsValid() bool {
return len(feishuRobotSender.config) > 0
}
func (feishuRobotSender *SFeishuRobotSender) IsPullType() bool {
return true
}
func (feishuRobotSender *SFeishuRobotSender) IsSystemConfigContactType() bool {
return true
}
func (feishuRobotSender *SFeishuRobotSender) GetAccessToken() error {
return nil
}
func init() {
models.Register(&SFeishuRobotSender{
config: map[string]api.SNotifyConfigContent{},
})
}
+113
View File
@@ -0,0 +1,113 @@
// 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 sender
import (
"yunion.io/x/cloudmux/pkg/cloudprovider"
"yunion.io/x/pkg/errors"
api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/notify/models"
)
type SMobileSender struct {
config map[string]api.SNotifyConfigContent
}
func (smsSender *SMobileSender) GetSenderType() string {
return api.MOBILE
}
func (smsSender *SMobileSender) Send(args api.SendParams) error {
smsSendParams := api.SSMSSendParams{
From: "",
To: args.Receivers.Contact,
}
smsdriver := models.GetSMSDriver(models.ConfigMap[api.MOBILE].Content.SmsDriver)
return smsdriver.Send(smsSendParams, false, &api.NotifyConfig{
SNotifyConfigContent: *models.ConfigMap[api.MOBILE].Content,
Attribution: models.ConfigMap[api.MOBILE].Attribution,
DomainId: models.ConfigMap[api.MOBILE].DomainId,
})
}
func (smsSender *SMobileSender) ValidateConfig(config api.NotifyConfig) (string, error) {
driver := models.GetSMSDriver(config.SmsDriver)
return "", driver.Verify(&config)
}
func (smsSender *SMobileSender) UpdateConfig(config api.NotifyConfig) error {
q := models.ConfigManager.Query()
q = q.Equals("type", api.MOBILE)
confs := []models.SConfig{}
db.FetchModelObjects(models.ConfigManager, q, &confs)
if len(confs) == 0 {
return errors.Wrapf(errors.ErrNotFound, "config type:%s", api.MOBILE)
}
_, err := db.Update(&confs[0], func() error {
confs[0].Content = &config.SNotifyConfigContent
return nil
})
if err != nil {
return errors.Wrap(err, "update config")
}
models.ConfigMap[api.MOBILE] = models.SConfig{
Content: &config.SNotifyConfigContent,
}
return nil
}
func (smsSender *SMobileSender) AddConfig(config api.NotifyConfig) error {
return cloudprovider.ErrNotImplemented
}
func (smsSender *SMobileSender) DeleteConfig(config api.NotifyConfig) error {
return cloudprovider.ErrNotImplemented
}
func (smsSender *SMobileSender) ContactByMobile(mobile, domainId string) (string, error) {
return "", nil
}
func (smsSender *SMobileSender) IsPersonal() bool {
return true
}
func (smsSender *SMobileSender) IsRobot() bool {
return false
}
func (smsSender *SMobileSender) IsValid() bool {
return len(smsSender.config) > 0
}
func (smsSender *SMobileSender) IsPullType() bool {
return false
}
func (smsSender *SMobileSender) IsSystemConfigContactType() bool {
return true
}
func (smsSender *SMobileSender) GetAccessToken() error {
return nil
}
func init() {
models.Register(&SMobileSender{
config: map[string]api.SNotifyConfigContent{},
})
}
+130
View File
@@ -0,0 +1,130 @@
// 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 smsdriver
import (
"encoding/json"
"regexp"
"github.com/aliyun/alibaba-cloud-sdk-go/sdk"
sdkerrors "github.com/aliyun/alibaba-cloud-sdk-go/sdk/errors"
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests"
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/responses"
"yunion.io/x/pkg/errors"
api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/notify/models"
)
type SAliyunSMSDriver struct{}
var parser = regexp.MustCompile(`\+(\d*) (.*)`)
func (d *SAliyunSMSDriver) Name() string {
return DriverAliyun
}
func (d *SAliyunSMSDriver) Verify(config *api.NotifyConfig) error {
err := d.Send(api.SSMSSendParams{}, true, config)
if err == ErrSignnameInvalid || err == ErrSignatureDoesNotMatch || err == ErrAccessKeyIdNotFound {
return nil
}
return errors.Wrap(err, "Verify")
}
func (d *SAliyunSMSDriver) Send(args api.SSMSSendParams, isVerify bool, config *api.NotifyConfig) error {
if isVerify {
args.AppKey = config.AccessKeyId
args.AppSecret = config.AccessKeySecret
args.Signature = config.Signature
} else {
args.AppKey = models.ConfigMap[api.MOBILE].Content.AccessKeyId
args.AppSecret = models.ConfigMap[api.MOBILE].Content.AccessKeySecret
args.Signature = models.ConfigMap[api.MOBILE].Content.Signature
}
return d.sendSms(args)
}
func (d *SAliyunSMSDriver) sendSms(args api.SSMSSendParams) error {
// lock and update
client, err := sdk.NewClientWithAccessKey("default", args.AppKey, args.AppSecret)
if err != nil {
return err
}
m := parser.FindStringSubmatch(args.To)
if len(m) > 0 {
if m[1] == "86" {
args.To = m[2]
} else {
args.To = m[1] + m[2]
}
}
request := requests.NewCommonRequest()
request.Method = "POST"
request.Scheme = "https" // https | http
request.Domain = "dysmsapi.aliyuncs.com"
request.Version = "2017-05-25"
request.ApiName = "SendSms"
request.QueryParams["RegionId"] = "default"
request.QueryParams["PhoneNumbers"] = args.To
request.QueryParams["SignName"] = args.Signature
request.QueryParams["TemplateCode"] = args.TemplateId
request.QueryParams["TemplateParam"] = args.TemplateParas
return d.checkResponseAndError(client.ProcessCommonRequest(request))
}
func (d *SAliyunSMSDriver) checkResponseAndError(rep *responses.CommonResponse, err error) error {
if err != nil {
serr, ok := err.(*sdkerrors.ServerError)
if !ok {
return err
}
if serr.ErrorCode() == ACCESSKEYID_NOTFOUND {
return ErrAccessKeyIdNotFound
}
if serr.ErrorCode() == SIGN_DOESNOTMATCH {
return ErrSignatureDoesNotMatch
}
return err
}
type RepContent struct {
Message string
Code string
}
respContent := rep.GetHttpContentBytes()
rc := RepContent{}
err = json.Unmarshal(respContent, &rc)
if err != nil {
return errors.Wrap(err, "json.Unmarshal")
}
if rc.Code == "OK" {
return nil
}
if rc.Code == SIGHNTURE_ILLEGAL {
return ErrSignnameInvalid
} else if rc.Code == TEMPLATE_ILLGAL {
return ErrSignnameInvalid
}
return errors.Error(rc.Message)
}
func init() {
models.SMSRegister(&SAliyunSMSDriver{})
}
+40
View File
@@ -0,0 +1,40 @@
package smsdriver
import "yunion.io/x/pkg/errors"
const (
DriverKey = "sms_driver"
DriverAliyun = "smsaliyun"
DriverHuawei = "smshuawei"
)
const (
ACCESS_KEY_ID = "access_key_id"
ACCESS_KEY_SECRET = "access_key_secret"
SIGNATURE = "signature"
SERVICE_URL = "service_url"
)
var (
ErrAccessKeyIdNotFound = errors.Error("AccessKeyId not found")
ErrSignatureDoesNotMatch = errors.Error("AccessKeySecret does not match with the accessKeyId")
ErrSignnameInvalid = errors.Error("Invalid signature (does not exist or is blackened)")
ErrDriverNotFound = errors.Error("Driver not found")
)
const (
ACESS_KEY_ID_BP = "accessKeyId"
ACESS_KEY_SECRET_BP = "accessKeySecret"
NEED_REMOTE_TEMPLATE = "remote template is needed in aliyun sms"
ACCESSKEYID_NOTFOUND = "InvalidAccessKeyId.NotFound"
SIGN_DOESNOTMATCH = "SignatureDoesNotMatch"
SIGHNTURE_ILLEGAL = "isv.SMS_SIGNATURE_ILLEGAL"
TEMPLATE_ILLGAL = "isv.SMS_TEMPLATE_ILLEGAL"
)
const (
HuaweiSendUri = "/sms/batchSendSms/v1"
)
@@ -12,4 +12,4 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package apis // import "yunion.io/x/onecloud/pkg/notify/rpc/apis"
package smsdriver // import "yunion.io/x/onecloud/pkg/notify/sender"
+102
View File
@@ -0,0 +1,102 @@
// 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 smsdriver
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"net/http"
"net/url"
"strings"
"time"
uuid "github.com/satori/go.uuid"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/httputils"
api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/notify/models"
)
//无需修改,用于格式化鉴权头域,给"X-WSSE"参数赋值
const WSSE_HEADER_FORMAT = "UsernameToken Username=\"%s\",PasswordDigest=\"%s\",Nonce=\"%s\",Created=\"%s\""
//无需修改,用于格式化鉴权头域,给"Authorization"参数赋值
const AUTH_HEADER_VALUE = "WSSE realm=\"SDP\",profile=\"UsernameToken\",type=\"Appkey\""
type SHuaweiSMSDriver struct{}
func (d *SHuaweiSMSDriver) Name() string {
return DriverHuawei
}
func (d *SHuaweiSMSDriver) Verify(config *api.NotifyConfig) error {
err := d.Send(api.SSMSSendParams{}, true, config)
if err == ErrSignnameInvalid || err == ErrSignatureDoesNotMatch || err == ErrAccessKeyIdNotFound {
return nil
}
return errors.Wrap(err, "Verify")
}
func (d *SHuaweiSMSDriver) Send(args api.SSMSSendParams, isVerify bool, config *api.NotifyConfig) error {
if isVerify {
args.AppKey = config.AccessKeyId
args.AppSecret = config.AccessKeySecret
args.Signature = config.Signature
} else {
args.AppKey = models.ConfigMap[api.MOBILE].Content.AccessKeyId
args.AppSecret = models.ConfigMap[api.MOBILE].Content.AccessKeySecret
args.Signature = models.ConfigMap[api.MOBILE].Content.Signature
}
return d.sendSms(args)
}
func (d *SHuaweiSMSDriver) sendSms(args api.SSMSSendParams) error {
uri := models.ConfigMap[api.MOBILE].Content.ServiceUrl + HuaweiSendUri + "?"
header := http.Header{}
header.Set("Content-Type", "application/x-www-form-urlencoded")
header.Set("Authorization", AUTH_HEADER_VALUE)
header.Set("X-WSSE", buildWsseHeader(args.AppKey, args.AppSecret))
params := url.Values{}
params.Set("from", args.From)
params.Set("to", args.To)
params.Set("templateId", args.TemplateId)
params.Set("templateParas", args.TemplateParas)
params.Set("signature", args.Signature)
_, err := sendRequest(uri, httputils.POST, header, params, nil)
if err != nil {
return errors.Wrap(err, "huawei sendRequest")
}
return nil
}
func buildWsseHeader(appKey, appSecret string) string {
var cTime = time.Now().Format("2006-01-02T15:04:05Z")
var nonce = uuid.NewV4().String()
nonce = strings.ReplaceAll(nonce, "-", "")
h := sha256.New()
h.Write([]byte(nonce + cTime + appSecret))
passwordDigestBase64Str := base64.StdEncoding.EncodeToString(h.Sum(nil))
return fmt.Sprintf(WSSE_HEADER_FORMAT, appKey, passwordDigestBase64Str, nonce, cTime)
}
func init() {
models.SMSRegister(&SHuaweiSMSDriver{})
}
+78
View File
@@ -0,0 +1,78 @@
// 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 smsdriver
import (
"bytes"
"context"
"io/ioutil"
"net/http"
"net/url"
"strings"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/errors"
utilerr "yunion.io/x/pkg/errors"
"yunion.io/x/pkg/gotypes"
"yunion.io/x/pkg/util/httputils"
)
const (
LARK_MESSAGE_SUCCESS = "success"
)
var (
cli = &http.Client{
Transport: httputils.GetTransport(true),
}
ctx = context.Background()
)
// 通知请求
func sendRequest(url string, method httputils.THttpMethod, header http.Header, params url.Values, body jsonutils.JSONObject) (jsonutils.JSONObject, error) {
var bodystr string
if !gotypes.IsNil(body) {
bodystr = body.String()
}
jbody := strings.NewReader(bodystr)
if header == nil {
header = http.Header{}
}
if header == nil {
header = http.Header{}
}
if len(header.Values("Content-Type")) == 0 {
header.Set("Content-Type", "application/json")
}
if params != nil {
url += params.Encode()
}
resp, err := httputils.Request(cli, ctx, method, url, header, jbody, true)
if err != nil {
return nil, utilerr.Wrap(err, "http request")
}
rbody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
rbody = bytes.TrimSpace(rbody)
var jrbody jsonutils.JSONObject = nil
if len(rbody) > 0 && (rbody[0] == '{' || rbody[0] == '[') {
jrbody, _ = jsonutils.Parse(rbody)
} else {
return nil, errors.Wrap(err, "resource not json")
}
return jrbody, nil
}
+55
View File
@@ -0,0 +1,55 @@
// 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 sender
import (
"context"
"net/http"
"net/url"
"yunion.io/x/jsonutils"
utilerr "yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/httputils"
"yunion.io/x/onecloud/pkg/notify/options"
)
const (
LARK_MESSAGE_SUCCESS = "success"
)
var (
cli = &http.Client{
Transport: httputils.GetTransport(true),
}
ctx = context.Background()
)
// 通知请求
func sendRequest(url string, method httputils.THttpMethod, header http.Header, params url.Values, body jsonutils.JSONObject) (jsonutils.JSONObject, error) {
if header == nil {
header = http.Header{}
}
if len(header.Values("Content-Type")) == 0 {
header.Set("Content-Type", "application/json")
}
if params != nil {
url += params.Encode()
}
_, req, err := httputils.JSONRequest(cli, ctx, method, url, header, body, options.Options.DebugRequest)
if err != nil {
return nil, utilerr.Wrap(err, "http request")
}
return req, nil
}
+118
View File
@@ -0,0 +1,118 @@
// 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 sender
import (
"yunion.io/x/cloudmux/pkg/cloudprovider"
api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/notify/models"
)
type SWebconsoleSender struct {
config map[string]api.SNotifyConfigContent
}
func (self *SWebconsoleSender) GetSenderType() string {
return api.WEBCONSOLE
}
func (self *SWebconsoleSender) Send(args api.SendParams) error {
// var token string
// var errs []error
// title, msg := args.Title, args.Message
// // for _, recevier := range args.Receivers {
// webhook := args.Receivers.Contact
// switch {
// case strings.HasPrefix(webhook, ApiWebhookRobotV2SendMessage):
// token = webhook[len(ApiWebhookRobotV2SendMessage):]
// case strings.HasPrefix(webhook, feishu.ApiWebhookRobotSendMessage):
// token = webhook[len(feishu.ApiWebhookRobotSendMessage):]
// default:
// return errors.Wrap(InvalidWebhook, webhook)
// }
// req := feishu.WebhookRobotMsgReq{
// Title: title,
// Text: msg,
// }
// rep, err := feishu.SendWebhookRobotMessage(token, req)
// if err != nil {
// return errors.Wrap(err, "SendWebhookRobotMessage")
// }
// if !rep.Ok {
// if strings.Contains(rep.Error, "token") {
// return ErrNoSuchWebhook
// } else {
// return fmt.Errorf("SendWebhookRobotMessage failed: %s", rep.Error)
// }
// }
// if err != nil {
// if errs == nil {
// errs = []error{}
// }
// errs = append(errs, err)
// }
// return errors.NewAggregate(errs)
return nil
}
func (websender *SWebconsoleSender) ValidateConfig(config api.NotifyConfig) (string, error) {
return "", cloudprovider.ErrNotImplemented
}
func (websender *SWebconsoleSender) UpdateConfig(config api.NotifyConfig) error {
return cloudprovider.ErrNotImplemented
}
func (websender *SWebconsoleSender) AddConfig(config api.NotifyConfig) error {
return cloudprovider.ErrNotImplemented
}
func (websender *SWebconsoleSender) DeleteConfig(config api.NotifyConfig) error {
return cloudprovider.ErrNotImplemented
}
func (websender *SWebconsoleSender) ContactByMobile(mobile, domainId string) (string, error) {
return "", cloudprovider.ErrNotImplemented
}
func (websender *SWebconsoleSender) IsPersonal() bool {
return true
}
func (websender *SWebconsoleSender) IsRobot() bool {
return true
}
func (websender *SWebconsoleSender) IsValid() bool {
return len(websender.config) > 0
}
func (websender *SWebconsoleSender) IsPullType() bool {
return true
}
func (websender *SWebconsoleSender) IsSystemConfigContactType() bool {
return true
}
func (websender *SWebconsoleSender) GetAccessToken() error {
return nil
}
func init() {
models.Register(&SWebconsoleSender{
config: map[string]api.SNotifyConfigContent{},
})
}
+103
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 sender
import (
"net/http"
"strings"
"yunion.io/x/cloudmux/pkg/cloudprovider"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/util/httputils"
api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/notify/models"
)
type SWebhookSender struct {
config map[string]api.SNotifyConfigContent
}
func (self *SWebhookSender) GetSenderType() string {
return api.WEBHOOK_ROBOT
}
func (self *SWebhookSender) Send(args api.SendParams) error {
log.Infoln("this is in webhookSend ")
body, err := jsonutils.ParseString(args.Message)
if err != nil {
log.Errorf("unable to parse %q: %v", args.Message, err)
}
if _, ok := body.(*jsonutils.JSONString); err != nil || ok {
dict := jsonutils.NewDict()
dict.Set("Msg", jsonutils.NewString(args.Message))
body = dict
}
event := strings.ToUpper(args.Event)
header := http.Header{}
header.Set(EVENT_HEADER, event)
_, _, err = httputils.JSONRequest(cli, ctx, httputils.POST, args.Receivers.Contact, header, body, true)
return err
}
func (websender *SWebhookSender) ValidateConfig(config api.NotifyConfig) (string, error) {
return "", cloudprovider.ErrNotImplemented
}
func (websender *SWebhookSender) UpdateConfig(config api.NotifyConfig) error {
return cloudprovider.ErrNotImplemented
}
func (websender *SWebhookSender) AddConfig(config api.NotifyConfig) error {
return cloudprovider.ErrNotImplemented
}
func (websender *SWebhookSender) DeleteConfig(config api.NotifyConfig) error {
return cloudprovider.ErrNotImplemented
}
func (websender *SWebhookSender) ContactByMobile(mobile, domainId string) (string, error) {
return "", cloudprovider.ErrNotImplemented
}
func (websender *SWebhookSender) IsPersonal() bool {
return true
}
func (websender *SWebhookSender) IsRobot() bool {
return true
}
func (websender *SWebhookSender) IsValid() bool {
return len(websender.config) > 0
}
func (websender *SWebhookSender) IsPullType() bool {
return true
}
func (websender *SWebhookSender) IsSystemConfigContactType() bool {
return true
}
func (websender *SWebhookSender) GetAccessToken() error {
return nil
}
func init() {
models.Register(&SWebhookSender{
config: map[string]api.SNotifyConfigContent{},
})
}
+120
View File
@@ -0,0 +1,120 @@
// 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 sender
import (
"context"
"fmt"
"strings"
"yunion.io/x/cloudmux/pkg/cloudprovider"
"yunion.io/x/jsonutils"
"yunion.io/x/onecloud/pkg/ansibleserver/options"
api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/mcclient/auth"
modules "yunion.io/x/onecloud/pkg/mcclient/modules/websocket"
"yunion.io/x/onecloud/pkg/notify/models"
)
type SWebsocketSender struct {
config map[string]api.SNotifyConfigContent
}
func (websocket *SWebsocketSender) GetSenderType() string {
return api.WEBSOCKET
}
func (websocket *SWebsocketSender) Send(params api.SendParams) error {
return websocket.send(params)
}
func (websocket *SWebsocketSender) send(args api.SendParams) error {
params := jsonutils.NewDict()
params.Add(jsonutils.NewString("notify"), "obj_type")
params.Add(jsonutils.NewString(""), "obj_id")
params.Add(jsonutils.NewString(""), "obj_name")
params.Add(jsonutils.JSONTrue, "success")
// component request body
body := jsonutils.DeepCopy(params).(*jsonutils.JSONDict)
body.Add(jsonutils.NewString(args.Title), "action")
body.Add(jsonutils.NewString(fmt.Sprintf("priority=%s; content=%s", args.Priority, args.Message)), "notes")
body.Add(jsonutils.NewString(args.Receivers.Contact), "user_id")
body.Add(jsonutils.NewString(args.Receivers.Contact), "user")
if len(args.Receivers.Contact) == 0 {
body.Add(jsonutils.JSONTrue, "broadcast")
}
if websocket.isFailed(args.Title, args.Message) {
body.Add(jsonutils.JSONFalse, "success")
}
session := auth.GetAdminSession(context.Background(), options.Options.Region)
_, err := modules.Websockets.Create(session, body)
if err != nil {
// failed
_, err = modules.Websockets.Create(session, body)
return err
}
return nil
}
func (websocket *SWebsocketSender) isFailed(title, message string) bool {
for _, c := range []string{title, message} {
for _, k := range FAIL_KEY {
if strings.Contains(c, k) {
return true
}
}
}
return false
}
func (websocket *SWebsocketSender) IsPersonal() bool {
return true
}
func (websocket *SWebsocketSender) IsRobot() bool {
return false
}
func (websocket *SWebsocketSender) IsValid() bool {
return true
}
func (websocket *SWebsocketSender) IsPullType() bool {
return true
}
func (websocket *SWebsocketSender) IsSystemConfigContactType() bool {
return true
}
func (websocket *SWebsocketSender) ContactByMobile(mobile, domainId string) (string, error) {
return "", nil
}
func (websocket *SWebsocketSender) GetAccessToken() error {
return nil
}
func (websocket *SWebsocketSender) ValidateConfig(config api.NotifyConfig) (string, error) {
return "", cloudprovider.ErrNotImplemented
}
func init() {
models.Register(&SWebsocketSender{
config: map[string]api.SNotifyConfigContent{},
})
}
+140
View File
@@ -0,0 +1,140 @@
// 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 sender
import (
"fmt"
"net/http"
"net/url"
"strings"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/httputils"
api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/notify/models"
)
type SWorkwxSender struct {
config map[string]api.SNotifyConfigContent
}
func (workwxSender *SWorkwxSender) GetSenderType() string {
return api.WORKWX
}
func (workwxSender *SWorkwxSender) Send(args api.SendParams) error {
body := map[string]interface{}{
"agentid": models.ConfigMap[api.WORKWX].Content.AgentId,
"msgtype": "markdown",
"markdown": map[string]interface{}{
"content": fmt.Sprintf("# %s\n\n%s", args.Title, args.Message),
},
"touser": args.Receivers.Contact,
}
_, err := workwxSender.sendMessageWithToken(ApiWorkwxSendMessage, httputils.POST, nil, nil, jsonutils.Marshal(body))
if err != nil {
return errors.Wrap(err, "workwx send message")
}
return err
}
func (workwxSender *SWorkwxSender) ValidateConfig(config api.NotifyConfig) (string, error) {
// 校验accesstoken
_, err := workwxSender.getAccessToken(config.CorpId, config.Secret)
if err != nil {
switch {
case strings.Contains(err.Error(), "40013"):
return "invalid corpid", nil
case strings.Contains(err.Error(), "40001"):
return "invalid corpsecret", nil
}
return "", err
}
return "", nil
}
func (workwxSender *SWorkwxSender) ContactByMobile(mobile, domainId string) (string, error) {
err := workwxSender.GetAccessToken()
if err != nil {
return "", err
}
body := jsonutils.Marshal(map[string]interface{}{
"mobile": mobile,
})
res, err := workwxSender.sendMessageWithToken(ApiWorkwxGetUserByMobile, httputils.POST, nil, nil, jsonutils.Marshal(body))
if err != nil {
return "", errors.Wrap(err, "get user by mobile")
}
return res.GetString("userid")
}
func (workwxSender *SWorkwxSender) IsPersonal() bool {
return true
}
func (workwxSender *SWorkwxSender) IsRobot() bool {
return false
}
func (workwxSender *SWorkwxSender) IsValid() bool {
return len(workwxSender.config) > 0
}
func (workwxSender *SWorkwxSender) IsPullType() bool {
return true
}
func (workwxSender *SWorkwxSender) IsSystemConfigContactType() bool {
return true
}
func (workwxSender *SWorkwxSender) GetAccessToken() error {
corpId, secret := models.ConfigMap[api.WORKWX].Content.CorpId, models.ConfigMap[api.WORKWX].Content.Secret
token, err := workwxSender.getAccessToken(corpId, secret)
if err != nil {
return errors.Wrap(err, "workwx getAccessToken")
}
models.ConfigMap[api.WORKWX].Content.AccessToken = token
return nil
}
func (workwxSender *SWorkwxSender) getAccessToken(corpId, secret string) (string, error) {
// url := ApiWorkwxGetToken + fmt.Sprintf("?corpid=%s&corpsecret=%s", corpId, secret)
params := url.Values{}
params.Set("corpid", corpId)
params.Set("corpsecret", secret)
res, err := sendRequest(ApiWorkwxGetToken, httputils.GET, nil, params, nil)
if err != nil {
return "", errors.Wrap(err, "get workwx token")
}
return res.GetString("access_token")
}
func (workwxSender *SWorkwxSender) sendMessageWithToken(uri string, method httputils.THttpMethod, header http.Header, params url.Values, body jsonutils.JSONObject) (jsonutils.JSONObject, error) {
if params == nil {
params = url.Values{}
}
params.Set("access_token", models.ConfigMap[api.WORKWX].Content.AccessToken)
return sendRequest(uri, httputils.POST, nil, params, jsonutils.Marshal(body))
}
func init() {
models.Register(&SWorkwxSender{
config: map[string]api.SNotifyConfigContent{},
})
}
+95
View File
@@ -0,0 +1,95 @@
// 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 sender
import (
"fmt"
"yunion.io/x/cloudmux/pkg/cloudprovider"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/httputils"
api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/notify/models"
)
type SWorkwxRobotSender struct {
config map[string]api.SNotifyConfigContent
}
func (workwxRobotSender *SWorkwxRobotSender) GetSenderType() string {
return api.WORKWX_ROBOT
}
func (workwxRobotSender *SWorkwxRobotSender) Send(args api.SendParams) error {
errs := []error{}
content := fmt.Sprintf("# %s\n\n%s", args.Title, args.Message)
mid := map[string]interface{}{
"msgtype": "markdown",
"markdown": map[string]interface{}{
"content": content,
},
}
req, err := sendRequest(args.Receivers.Contact, httputils.POST, nil, nil, jsonutils.Marshal(mid))
if err != nil {
return errors.Wrap(err, "sendRequest")
}
errCode, err := req.GetString("errcode")
if err != nil {
return errors.Wrap(err, "req.GetString")
}
if errCode != "0" {
errs = append(errs, errors.Errorf(req.PrettyString()))
}
return errors.NewAggregate(errs)
}
func (workwxRobotSender *SWorkwxRobotSender) ValidateConfig(config api.NotifyConfig) (string, error) {
return "", cloudprovider.ErrNotImplemented
}
func (workwxRobotSender *SWorkwxRobotSender) ContactByMobile(mobile, domainId string) (string, error) {
return "", cloudprovider.ErrNotImplemented
}
func (workwxRobotSender *SWorkwxRobotSender) IsPersonal() bool {
return true
}
func (workwxRobotSender *SWorkwxRobotSender) IsRobot() bool {
return true
}
func (workwxRobotSender *SWorkwxRobotSender) IsValid() bool {
return len(workwxRobotSender.config) > 0
}
func (workwxRobotSender *SWorkwxRobotSender) IsPullType() bool {
return true
}
func (workwxRobotSender *SWorkwxRobotSender) IsSystemConfigContactType() bool {
return true
}
func (workwxRobotSender *SWorkwxRobotSender) GetAccessToken() error {
return nil
}
func init() {
models.Register(&SWorkwxRobotSender{
config: map[string]api.SNotifyConfigContent{},
})
}
+1 -8
View File
@@ -20,7 +20,7 @@ import (
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
"yunion.io/x/onecloud/pkg/notify/models"
"yunion.io/x/onecloud/pkg/notify/oldmodels"
_ "yunion.io/x/onecloud/pkg/notify/sender"
)
const (
@@ -37,13 +37,6 @@ func InitHandlers(app *appsrv.Application) {
db.AddScopeResourceCountHandler(API_VERSION, app)
// Data migration
db.RegisterModelManager(oldmodels.NotificationManager)
db.RegisterModelManager(oldmodels.ContactManager)
db.RegisterModelManager(oldmodels.ConfigManager)
db.RegisterModelManager(oldmodels.TemplateManager)
db.RegisterModelManager(oldmodels.UserCacheManager)
taskman.AddTaskHandler(API_VERSION, app)
for _, manager := range []db.IModelManager{
taskman.TaskManager,
+7 -10
View File
@@ -19,6 +19,7 @@ import (
"time"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
_ "yunion.io/x/sqlchemy/backends"
api "yunion.io/x/onecloud/pkg/apis/notify"
@@ -30,7 +31,7 @@ import (
"yunion.io/x/onecloud/pkg/notify/models"
"yunion.io/x/onecloud/pkg/notify/options"
_ "yunion.io/x/onecloud/pkg/notify/policy"
"yunion.io/x/onecloud/pkg/notify/rpc"
_ "yunion.io/x/onecloud/pkg/notify/sender/smsdriver"
_ "yunion.io/x/onecloud/pkg/notify/tasks"
)
@@ -60,19 +61,15 @@ func StartService() {
db.EnsureAppSyncDB(applicaion, dbOpts, models.InitDB)
defer cloudcommon.CloseDB()
err := models.ReceiverManager.StartWatchUserInKeystone()
if err != nil {
log.Logger().Panic(err.Error())
if options.Options.EnableWatchUser {
err := models.ReceiverManager.StartWatchUserInKeystone()
if err != nil {
log.Errorln(errors.Wrap(err, "StartWatchUserInKeystone"))
}
}
// init notify service
models.NotifyService = rpc.NewSRpcService(opts.SocketFileDir, models.ConfigManager, models.TemplateManager)
models.NotifyService.InitAll()
defer models.NotifyService.StopAll()
cron := cronman.InitCronJobManager(true, 2)
// update service
cron.AddJobAtIntervals("UpdateServices", time.Duration(opts.UpdateInterval)*time.Minute, models.NotifyService.UpdateServices)
cron.AddJobAtIntervalsWithStartRun("syncReciverFromKeystone", time.Duration(opts.SyncReceiverIntervalMinutes)*time.Minute, models.ReceiverManager.SyncUserFromKeystone, true)
// wrapped func to resend notifications
+91 -90
View File
@@ -22,13 +22,13 @@ import (
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
apis "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
"yunion.io/x/onecloud/pkg/notify/models"
"yunion.io/x/onecloud/pkg/notify/options"
rpcapi "yunion.io/x/onecloud/pkg/notify/rpc/apis"
"yunion.io/x/onecloud/pkg/util/logclient"
)
@@ -70,21 +70,27 @@ func (self *NotificationSendTask) OnInit(ctx context.Context, obj db.IStandalone
}
rns, err := notification.ReceiverNotificationsNotOK()
if err != nil {
self.taskFailed(ctx, notification, "fail to fetch ReceiverNotifications", true)
self.taskFailed(ctx, notification, errors.Wrapf(err, "ReceiverNotificationsNotOK").Error(), true)
return
}
event, err := models.EventManager.GetEvent(notification.EventId)
if err != nil {
self.taskFailed(ctx, notification, errors.Wrapf(err, "GetEvent").Error(), true)
return
}
notification.SetStatus(self.UserCred, apis.NOTIFICATION_STATUS_SENDING, "")
// build contactMap
receivers := make([]ReceiverSpec, 0)
receiversEn := make([]ReceiverSpec, 0, len(rns)/2)
receiversCn := make([]ReceiverSpec, 0, len(rns)/2)
failedRecord := make([]string, 0)
sendFail := func(rn *models.SReceiverNotification, reason string) {
rn.AfterSend(ctx, false, reason)
failedRecord = append(failedRecord, fmt.Sprintf("%s: %s", rn.ReceiverID, reason))
}
// build contactMap
receivers := make([]ReceiverSpec, 0, 0)
receiversEn := make([]ReceiverSpec, 0, len(rns)/2)
receiversCn := make([]ReceiverSpec, 0, len(rns)/2)
for i := range rns {
receiver, err := rns[i].Receiver()
if err != nil {
@@ -99,10 +105,18 @@ func (self *NotificationSendTask) OnInit(ctx context.Context, obj db.IStandalone
// check contact enabled
enabled, err := receiver.IsEnabledContactType(notification.ContactType)
if err != nil {
sendFail(&rns[i], fmt.Sprintf("IsEnabledContactType error for receiver: %s", err.Error()))
logclient.AddSimpleActionLog(notification, logclient.ACT_SEND_NOTIFICATION, errors.Wrapf(err, "GetEnabledContactTypes"), self.GetUserCred(), false)
continue
}
if !enabled {
if notification.ContactType == apis.WEBHOOK {
notification.ContactType = apis.WEBHOOK_ROBOT
}
if receiver.IsRobot() {
robot := receiver.(*models.SRobot)
notification.ContactType = fmt.Sprintf("%s-robot", robot.Type)
}
driver := models.GetDriver(notification.ContactType)
if driver == nil || !enabled {
sendFail(&rns[i], fmt.Sprintf("disabled contactType %q", notification.ContactType))
continue
}
@@ -120,10 +134,32 @@ func (self *NotificationSendTask) OnInit(ctx context.Context, obj db.IStandalone
// contact, err := receiver.GetContact(notification.ContactType)
// if err != nil {
// reason := fmt.Sprintf("fail to fetch contact: %s", err.Error())
// logclient.AddSimpleActionLog(notification, logclient.ACT_SEND_NOTIFICATION, errors.Wrapf(err, "GetContact(%s)", notification.ContactType), self.GetUserCred(), false)
// continue
// }
// notifyRecv := receiver.GetNotifyReceiver()
// notifyRecv.Lang, _ = receiver.GetTemplateLang(ctx)
// notifyRecv.Contact = contact
// cv := ReceiverSpec{
// receiver: notifyRecv,
// rNotificaion: &rns[i],
// }
// switch notifyRecv.Lang {
// case "":
// receivers = append(receivers, cv)
// case apis.TEMPLATE_LANG_EN:
// receiversEn = append(receiversEn, cv)
// case apis.TEMPLATE_LANG_CN:
// receiversCn = append(receiversCn, cv)
// }
// lang, err := receiver.GetTemplateLang(ctx)
// if err != nil {
// reason := fmt.Sprintf("fail to GetTemplateLang: %s", err.Error())
// sendFail(&rns[i], reason)
// continue
// }
lang, err := receiver.GetTemplateLang(ctx)
if err != nil {
reason := fmt.Sprintf("fail to GetTemplateLang: %s", err.Error())
@@ -148,7 +184,12 @@ func (self *NotificationSendTask) OnInit(ctx context.Context, obj db.IStandalone
})
}
}
var contactLen int
nn, err := notification.Notification()
if err != nil {
self.taskFailed(ctx, notification, errors.Wrapf(err, "Notification").Error(), true)
return
}
for lang, receivers := range map[string][]ReceiverSpec{
"": receivers,
apis.TEMPLATE_LANG_CN: receiversCn,
@@ -158,17 +199,13 @@ func (self *NotificationSendTask) OnInit(ctx context.Context, obj db.IStandalone
log.Warningf("no receiver to send, skip ...")
continue
}
// send
nn, err := notification.Notification()
p, err := notification.FillWithTemplate(ctx, lang, nn)
if err != nil {
self.taskFailed(ctx, notification, err.Error(), false)
logclient.AddSimpleActionLog(notification, logclient.ACT_SEND_NOTIFICATION, errors.Wrapf(err, "FillWithTemplate(%s)", lang), self.GetUserCred(), false)
continue
}
p, err := notification.TemplateStore().FillWithTemplate(ctx, lang, nn)
if err != nil {
self.taskFailed(ctx, notification, err.Error(), false)
}
p.Event = event.Event
switch lang {
case apis.TEMPLATE_LANG_CN:
p.Message += "\n来自 " + options.Options.ApiServer
@@ -181,11 +218,8 @@ func (self *NotificationSendTask) OnInit(ctx context.Context, obj db.IStandalone
for _, rn := range receivers {
rn.rNotificaion.BeforeSend(ctx, now)
}
contactLen += len(receivers)
// send
fds, err := self.batchSend(ctx, notification.ContactType, receivers, p)
fds, err := self.batchSend(ctx, notification, receivers, p)
if err != nil {
for _, r := range receivers {
sendFail(r.rNotificaion, err.Error())
@@ -224,77 +258,44 @@ type FailedReceiverSpec struct {
Reason string
}
func (self *NotificationSendTask) batchSend(ctx context.Context, contactType string, receivers []ReceiverSpec, params rpcapi.SendParams) (fails []FailedReceiverSpec, err error) {
log.Debugf("contactType: %s, receivers: %s, params: %s", contactType, jsonutils.Marshal(receivers), jsonutils.Marshal(params))
if contactType != apis.ROBOT && contactType != apis.WEBHOOK {
return self._batchSend(ctx, contactType, receivers, func(res []*rpcapi.SReceiver) ([]*rpcapi.FailedRecord, error) {
return models.NotifyService.BatchSend(ctx, contactType, rpcapi.BatchSendParams{
Receivers: res,
Title: params.Title,
Message: params.Message,
Priority: params.Priority,
RemoteTemplate: params.RemoteTemplate,
})
})
}
robots := make(map[string][]ReceiverSpec)
func (notificationSendTask *NotificationSendTask) batchSend(ctx context.Context, notification *models.SNotification, receivers []ReceiverSpec, params apis.SendParams) (fails []FailedReceiverSpec, err error) {
for i := range receivers {
robot := receivers[i].receiver.(*models.SRobot)
robots[robot.Type] = append(robots[robot.Type], receivers[i])
}
for rType, robots := range robots {
_fails, err := self._batchSend(ctx, contactType, robots, func(res []*rpcapi.SReceiver) ([]*rpcapi.FailedRecord, error) {
return models.NotifyService.SendRobotMessage(ctx, rType, res, params.Title, params.Message)
})
if err != nil {
for i := range robots {
fails = append(fails, FailedReceiverSpec{
ReceiverSpec: robots[i],
Reason: err.Error(),
})
if receivers[i].receiver.IsRobot() {
robot := receivers[i].receiver.(*models.SRobot)
driver := models.GetDriver(fmt.Sprintf("%s-robot", robot.Type))
params.Receivers.Contact = robot.Address
err = driver.Send(params)
if err != nil {
fails = append(fails, FailedReceiverSpec{ReceiverSpec: receivers[i], Reason: err.Error()})
}
} else if receivers[i].receiver.IsReceiver() {
receiver := receivers[i].receiver.(*models.SReceiver)
params.Receivers.Contact, _ = receiver.GetContact(notification.ContactType)
driver := models.GetDriver(notification.ContactType)
if notification.ContactType == apis.EMAIL {
params.EmailMsg = &apis.SEmailMessage{
To: []string{receiver.Email},
Subject: params.Title,
Body: params.Message,
}
}
if notification.ContactType == apis.MOBILE {
params.Receivers.Contact = receiver.Mobile
}
err = driver.Send(params)
if err != nil {
fails = append(fails, FailedReceiverSpec{ReceiverSpec: receivers[i], Reason: err.Error()})
}
} else {
receiver := receivers[i].receiver.(*models.SContact)
params.Receivers.Contact, _ = receiver.GetContact(notification.ContactType)
driver := models.GetDriver(notification.ContactType)
err = driver.Send(params)
if err != nil {
fails = append(fails, FailedReceiverSpec{ReceiverSpec: receivers[i], Reason: err.Error()})
}
}
fails = append(fails, _fails...)
}
return fails, nil
}
func (self *NotificationSendTask) _batchSend(ctx context.Context, contactType string, receivers []ReceiverSpec, send func([]*rpcapi.SReceiver) ([]*rpcapi.FailedRecord, error)) (fails []FailedReceiverSpec, err error) {
rpcReceivers := make([]*rpcapi.SReceiver, len(receivers))
rpc2Receiver := make(map[DomainContact]ReceiverSpec, len(receivers))
for i := range receivers {
contact, err := receivers[i].receiver.GetContact(contactType)
if err != nil {
fails = append(fails, FailedReceiverSpec{
ReceiverSpec: receivers[i],
Reason: fmt.Sprintf("fail to fetch contact: %s", err.Error()),
})
continue
}
rpcReceivers[i] = &rpcapi.SReceiver{
DomainId: receivers[i].receiver.GetDomainId(),
Contact: contact,
}
rpc2Receiver[DomainContact{
DomainId: receivers[i].receiver.GetDomainId(),
Contact: contact,
}] = receivers[i]
}
fds, err := send(rpcReceivers)
if err != nil {
return nil, err
}
// check result
for _, fd := range fds {
dc := DomainContact{
DomainId: fd.Receiver.DomainId,
Contact: fd.Receiver.Contact,
}
receiver := rpc2Receiver[dc]
fails = append(fails, FailedReceiverSpec{
ReceiverSpec: receiver,
Reason: fd.Reason,
})
}
return
}
+15 -15
View File
@@ -104,25 +104,25 @@ func (self *RepullSuncontactTask) OnInit(ctx context.Context, obj db.IStandalone
ctSets := sets.NewString(cts...)
if ctSets.Has(config.Type) {
ctSets.Delete(config.Type)
err = r.SetVerifiedContactTypes(ctSets.UnsortedList())
if err != nil {
reasons = append(reasons, repullFailedReason{
ReceiverId: r.Id,
Reason: fmt.Sprintf("unable to SetVerifiedContactTypes: %v", err),
}.String())
return
}
// err = r.SetVerifiedContactTypes(ctSets.UnsortedList())
// if err != nil {
// reasons = append(reasons, repullFailedReason{
// ReceiverId: r.Id,
// Reason: fmt.Sprintf("unable to SetVerifiedContactTypes: %v", err),
// }.String())
// return
// }
}
// pull
params := jsonutils.NewDict()
params.Set("contact_types", jsonutils.NewArray(jsonutils.NewString(config.Type)))
err = r.StartSubcontactPullTask(ctx, self.UserCred, params, self.Id)
if err != nil {
reasons = append(reasons, repullFailedReason{
ReceiverId: r.Id,
Reason: fmt.Sprintf("unable to StartSubcontactPullTask: %v", err),
}.String())
}
// err = r.StartSubcontactPullTask(ctx, self.UserCred, params, self.Id)
// if err != nil {
// reasons = append(reasons, repullFailedReason{
// ReceiverId: r.Id,
// Reason: fmt.Sprintf("unable to StartSubcontactPullTask: %v", err),
// }.String())
// }
}()
}
if len(reasons) > 0 {
+45 -16
View File
@@ -22,14 +22,15 @@ import (
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/tristate"
"yunion.io/x/pkg/utils"
"yunion.io/x/sqlchemy"
apis "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
"yunion.io/x/onecloud/pkg/mcclient/auth"
"yunion.io/x/onecloud/pkg/mcclient/modules/identity"
"yunion.io/x/onecloud/pkg/notify"
"yunion.io/x/onecloud/pkg/notify/models"
"yunion.io/x/onecloud/pkg/util/logclient"
)
@@ -84,35 +85,63 @@ func (self *SubcontactPullTask) OnInit(ctx context.Context, obj db.IStandaloneMo
} else {
contactTypes, _ = receiver.GetEnabledContactTypes()
}
for _, cType := range contactTypes {
if !utils.IsInStringArray(cType, PullContactType) {
continue
}
userid, err := models.NotifyService.ContactByMobile(ctx, receiver.Mobile, cType, receiver.GetDomainId())
content := ""
switch cType {
case apis.EMAIL:
content = receiver.Email
default:
driver := models.GetDriver(cType)
content, err = driver.ContactByMobile(mobile, "")
}
if err != nil {
var reason string
if errors.Cause(err) == notify.ErrNoSuchMobile {
receiver.MarkContactTypeUnVerified(cType, notify.ErrNoSuchMobile.Error())
if errors.Cause(err) == apis.ErrNoSuchMobile {
receiver.MarkContactTypeUnVerified(ctx, cType, apis.ErrNoSuchMobile.Error())
reason = fmt.Sprintf("%q: no such mobile %s", cType, receiver.Mobile)
} else if errors.Cause(err) == notify.ErrIncompleteConfig {
receiver.MarkContactTypeUnVerified(cType, notify.ErrIncompleteConfig.Error())
} else if errors.Cause(err) == apis.ErrIncompleteConfig {
receiver.MarkContactTypeUnVerified(ctx, cType, apis.ErrIncompleteConfig.Error())
reason = fmt.Sprintf("%q: %v", cType, err)
} else {
receiver.MarkContactTypeUnVerified(cType, "service exceptions")
receiver.MarkContactTypeUnVerified(ctx, cType, "service exceptions")
reason = fmt.Sprintf("%q: %v", cType, err)
}
failedReasons = append(failedReasons, reason)
continue
} else {
q := models.SubContactManager.Query()
cond := sqlchemy.AND(sqlchemy.Equals(q.Field("receiver_id"), receiver.Id), sqlchemy.Equals(q.Field("type"), cType))
q.Filter(cond)
subcontact := []models.SSubContact{}
err := db.FetchModelObjects(models.SubContactManager, q, &subcontact)
if err != nil {
failedReasons = append(failedReasons, err.Error())
continue
}
subid := ""
if len(subcontact) > 0 {
subid = subcontact[0].Id
}
err = models.SubContactManager.TableSpec().InsertOrUpdate(ctx, &models.SSubContact{
SStandaloneResourceBase: db.SStandaloneResourceBase{
SStandaloneAnonResourceBase: db.SStandaloneAnonResourceBase{Id: subid},
},
ReceiverID: receiver.Id,
Type: cType,
Contact: content,
ParentContactType: "mobile",
Enabled: tristate.True,
})
if err != nil {
log.Errorln("this is err:", err)
}
}
receiver.SetContact(cType, userid)
receiver.MarkContactTypeVerified(cType)
}
// push cache
err = receiver.PushCache(ctx)
if err != nil {
reason := fmt.Sprintf("PushCache: %v", err)
self.taskFailed(ctx, receiver, reason)
return
receiver.SetContact(cType, content)
receiver.MarkContactTypeVerified(ctx, cType)
}
if len(failedReasons) > 0 {
reason := strings.Join(failedReasons, "; ")
+302
View File
@@ -0,0 +1,302 @@
// 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 tasks
import (
"context"
"strings"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/tristate"
"yunion.io/x/pkg/utils"
api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
"yunion.io/x/onecloud/pkg/mcclient/auth"
identity "yunion.io/x/onecloud/pkg/mcclient/modules/identity"
"yunion.io/x/onecloud/pkg/notify/models"
"yunion.io/x/onecloud/pkg/notify/options"
"yunion.io/x/onecloud/pkg/util/logclient"
)
type TopicMessageSendTask struct {
taskman.STask
}
func init() {
taskman.RegisterTask(TopicMessageSendTask{})
}
func (topicMessageSendTask *TopicMessageSendTask) taskFailed(ctx context.Context, topic *models.STopic, err error) {
logclient.AddActionLogWithContext(ctx, topic, logclient.ACT_SEND_NOTIFICATION, err, topicMessageSendTask.UserCred, false)
topicMessageSendTask.SetStageFailed(ctx, jsonutils.NewString(err.Error()))
}
func (topicMessageSendTask *TopicMessageSendTask) OnInit(ctx context.Context, obj db.IStandaloneModel, body jsonutils.JSONObject) {
failedReasons := []string{}
topic := obj.(*models.STopic)
input := api.NotificationManagerEventNotifyInput{}
topicMessageSendTask.GetParams().Unmarshal(&input)
message := jsonutils.Marshal(input.ResourceDetails).String()
sevent := api.Event.WithAction(input.Action).WithResourceType(input.ResourceType)
event, err := models.EventManager.CreateEvent(ctx, sevent.String(), topic.Id, message, string(input.Action), string(input.ResourceType), input.AdvanceDays)
if err != nil {
topicMessageSendTask.taskFailed(ctx, topic, errors.Wrap(err, "unable to create Event"))
return
}
snotification := api.SsNotification{
Topic: topic.Id,
Message: event.Message,
Event: sevent,
AdvanceDays: event.AdvanceDays,
}
n := models.SNotification{
Priority: input.Priority,
EventId: event.GetId(),
TopicType: topic.Type,
}
n.Id = db.DefaultUUIDGenerator()
for _, contact := range input.ContactTypes {
n.ContactType = contact
err = models.NotificationManager.TableSpec().Insert(ctx, &n)
if err != nil {
topicMessageSendTask.taskFailed(ctx, topic, errors.Wrap(err, "notifications insert"))
return
}
}
if err != nil {
topicMessageSendTask.taskFailed(ctx, topic, errors.Wrap(err, "unable to fetch receivers by ids"))
return
}
needWebconsole := false
// 本地模板
send, _ := models.LocalTemplateManager.FillWithTemplate(ctx, api.TEMPLATE_LANG_CN, snotification)
// 远程模板(短信)
remoteSend, _ := models.TemplateManager.FillWithTemplate(ctx, api.TEMPLATE_LANG_CN, snotification)
send.Event = event.Event
remoteSend.Event = event.Event
websocketDriver := models.GetDriver(api.WEBSOCKET)
err = websocketDriver.Send(send)
if err != nil {
topicMessageSendTask.taskFailed(ctx, topic, errors.Wrapf(err, "websocket send"))
}
scribers, err := topic.GetEnabledSubscribers(input.ProjectDomainId, input.ProjectId)
if err != nil {
topicMessageSendTask.taskFailed(ctx, topic, errors.Wrapf(err, "GetSubscribers"))
return
}
robots := map[string]*models.SRobot{}
userIds := []string{}
for i := range scribers {
switch scribers[i].Type {
case api.SUBSCRIBER_TYPE_RECEIVER:
recvs, err := scribers[i].GetEnabledReceivers()
if err != nil {
log.Errorf("scribers[%d].GetEnabledReceivers err :%s", i, err)
return
}
// ids := []string{}
err = sendByReceivers(recvs, []string{}, n, send, remoteSend, needWebconsole)
if err != nil {
topicMessageSendTask.taskFailed(ctx, topic, errors.Wrap(err, "sendByReceivers"))
return
}
case api.SUBSCRIBER_TYPE_ROLE:
query := jsonutils.NewDict()
query.Set("roles", jsonutils.NewStringArray([]string{scribers[i].Identification}))
query.Set("effective", jsonutils.JSONTrue)
if scribers[i].RoleScope == api.SUBSCRIBER_SCOPE_DOMAIN {
query.Set("project_domain_id", jsonutils.NewString(scribers[i].ResourceAttributionId))
} else if scribers[i].RoleScope == api.SUBSCRIBER_SCOPE_PROJECT {
query.Add(jsonutils.NewString(scribers[i].ResourceAttributionId), "scope", "project", "id")
}
s := auth.GetAdminSession(ctx, options.Options.Region)
ret, err := identity.RoleAssignments.List(s, query)
if err != nil {
logclient.AddActionLogWithContext(ctx, topic, logclient.ACT_SEND_NOTIFICATION, errors.Wrapf(err, "RoleAssignments.List"), topicMessageSendTask.UserCred, false)
continue
}
users := []struct {
User struct {
Id string
}
}{}
jsonutils.Update(&users, ret.Data)
for _, user := range users {
userIds = append(userIds, user.User.Id)
}
recvs, err := models.ReceiverManager.FetchEnableReceiversByIdOrNames(ctx, userIds...)
if err != nil {
topicMessageSendTask.taskFailed(ctx, topic, errors.Wrap(err, "FetchEnableReceiversByIdOrNames"))
return
}
err = sendByReceivers(recvs, []string{}, n, send, remoteSend, false)
if err != nil {
topicMessageSendTask.taskFailed(ctx, topic, errors.Wrap(err, "sendByReceivers"))
return
}
case api.SUBSCRIBER_TYPE_ROBOT:
robot, err := scribers[i].GetRobot()
if err != nil {
logclient.AddActionLogWithContext(ctx, topic, logclient.ACT_SEND_NOTIFICATION, errors.Wrapf(err, "GetRobot"), topicMessageSendTask.UserCred, false)
continue
}
if !robot.Enabled.Bool() {
continue
}
robots[robot.Id] = robot
robotType := ""
switch robot.Type {
case api.FEISHU:
robotType = api.FEISHU_ROBOT
case api.DINGTALK:
robotType = api.DINGTALK_ROBOT
case api.WORKWX:
robotType = api.WORKWX_ROBOT
case api.WEBHOOK:
robotType = api.WEBHOOK_ROBOT
}
n.ContactType = robotType
err = models.NotificationManager.TableSpec().Insert(ctx, &n)
if err != nil {
failedReasons = append(failedReasons, err.Error())
}
session := auth.GetAdminSession(ctx, options.Options.Region)
rn := models.SReceiverNotification{
NotificationID: n.Id,
ReceiverType: api.RECEIVER_TYPE_ROBOT,
ReceiverID: robot.Id,
Status: api.RECEIVER_NOTIFICATION_RECEIVED,
SendBy: session.GetUserId(),
}
models.ReceiverNotificationManager.TableSpec().Insert(ctx, &rn)
driver := models.GetDriver(robotType)
if driver == nil {
log.Errorln(robotType)
}
send.Receivers = api.SNotifyReceiver{Contact: robot.Address}
err = driver.Send(send)
if err != nil {
log.Errorln("this is err:", err)
}
}
}
if len(failedReasons) > 0 {
reason := strings.Join(failedReasons, "; ")
topicMessageSendTask.taskFailed(ctx, topic, errors.Error(reason))
return
}
logclient.AddActionLogWithContext(ctx, topic, logclient.ACT_SEND_NOTIFICATION, jsonutils.Marshal(input), topicMessageSendTask.UserCred, true)
topicMessageSendTask.SetStageComplete(ctx, nil)
}
func sendByReceivers(recvs []models.SReceiver, receiverIds []string, n models.SNotification, send, remoteSend api.SendParams, needWebconsole bool) error {
ctx := context.Background()
failedReasons := []string{}
ids := []string{}
idFailedMap := make(map[string][]string)
for i, recv := range recvs {
// 检查是否发送email
if recvs[i].EnabledEmail == tristate.True {
if recvs[i].VerifiedEmail == tristate.True {
if recvs[i].EnabledEmail == tristate.True {
driver := models.GetDriver(api.EMAIL)
send.Receivers.Contact = recvs[i].Email
err := driver.Send(send)
if err != nil {
failedReasons = append(failedReasons, errors.Wrapf(err, "email send").Error())
}
}
} else {
log.Errorln(errors.Errorf("email has no verified: %s,receiver name: %s", recvs[i].Email, recvs[i].Name))
}
}
// 检查是否发送短信
if recvs[i].EnabledMobile == tristate.True {
if recvs[i].VerifiedMobile == tristate.True {
if recvs[i].EnabledMobile == tristate.True {
driver := models.GetDriver(api.MOBILE)
send.Receivers.Contact = recvs[i].Mobile
err := driver.Send(remoteSend)
if err != nil {
failedReasons = append(failedReasons, errors.Wrapf(err, "mobile send").Error())
}
}
} else {
log.Errorln(errors.Errorf("sms has no verified: %s,receiver name: %s", recvs[i].Mobile, recvs[i].Name))
}
}
idFailedMap[recv.Id] = append(idFailedMap[recv.Id], failedReasons...)
ids = append(ids, recvs[i].Id)
}
session := auth.GetAdminSession(ctx, options.Options.Region)
for _, receiverId := range receiverIds {
rn := models.SReceiverNotification{
NotificationID: n.Id,
Status: api.RECEIVER_NOTIFICATION_RECEIVED,
SendBy: session.GetUserId(),
}
if utils.IsInStringArray(receiverId, ids) {
rn.ReceiverType = api.RECEIVER_TYPE_USER
rn.ReceiverID = receiverId
} else {
rn.ReceiverType = api.RECEIVER_TYPE_CONTACT
rn.Contact = receiverId
}
models.ReceiverNotificationManager.TableSpec().Insert(ctx, &rn)
}
rm := &models.SReceiverManager{}
// 从subcontacts表中获取数据并发送
subContactsMap, err := rm.FetchSubContacts(ids)
if err != nil {
return errors.Wrap(err, "rm.FetchSubContacts")
}
for id, subContacts := range subContactsMap {
for _, subContact := range subContacts {
n.Topic = send.Topic
n.Status = api.NOTIFICATION_STATUS_SENDING
if subContact.Enabled == tristate.False {
continue
}
n.ContactType = subContact.Type
models.NotificationManager.TableSpec().Insert(context.Background(), &n)
driver := models.GetDriver(subContact.Type)
send.Receivers = api.SNotifyReceiver{
Contact: subContact.Contact,
}
err = driver.Send(send)
if err != nil {
failedReasons = append(failedReasons, errors.Wrapf(err, "content type:%s,receiver:%s", subContact.Type, subContact.ReceiverID).Error())
}
}
if len(idFailedMap[id]) == 0 {
}
}
return nil
}
func createReceiverNotification(receiverIds []string, recvs []models.SReceiver, n models.SNotification) {
}
+7 -9
View File
@@ -24,9 +24,7 @@ import (
api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
notifyv2 "yunion.io/x/onecloud/pkg/notify"
"yunion.io/x/onecloud/pkg/notify/models"
"yunion.io/x/onecloud/pkg/notify/rpc/apis"
"yunion.io/x/onecloud/pkg/util/logclient"
)
@@ -52,7 +50,6 @@ func (self *VerificationSendTask) OnInit(ctx context.Context, obj db.IStandalone
self.taskFailed(ctx, receiver, fmt.Sprintf("VerificationManager.Get for receiver_id %q and contact_type %q: %s", receiver.GetId(), contactType, err.Error()))
return
}
contact, err := receiver.GetContact(contactType)
if err != nil {
self.taskFailed(ctx, receiver, fmt.Sprintf("fail to get contact(type: %s): %s", contactType, err.Error()))
return
@@ -78,7 +75,7 @@ func (self *VerificationSendTask) OnInit(ctx context.Context, obj db.IStandalone
}
message = jsonutils.Marshal(data).String()
case api.MOBILE:
message = fmt.Sprintf(`[["code", "%s"]]`, verification.Token)
message = fmt.Sprintf("[\"%s\"]", verification.Token)
default:
// no way
}
@@ -86,8 +83,7 @@ func (self *VerificationSendTask) OnInit(ctx context.Context, obj db.IStandalone
if err != nil {
self.taskFailed(ctx, receiver, fmt.Sprintf("unable to GetTemplateLang for receiver %q: %v", receiver.Id, err))
}
param, err := models.TemplateManager.FillWithTemplate(ctx, tLang, notifyv2.SNotification{
param, err := models.TemplateManager.FillWithTemplate(ctx, tLang, api.SsNotification{
ContactType: contactType,
Topic: "VERIFY",
Message: message,
@@ -96,11 +92,13 @@ func (self *VerificationSendTask) OnInit(ctx context.Context, obj db.IStandalone
self.taskFailed(ctx, receiver, err.Error())
return
}
param.Receiver = &apis.SReceiver{
Contact: contact,
param.Receivers = api.SNotifyReceiver{
Contact: receiver.Mobile,
DomainId: receiver.DomainId,
}
err = models.NotifyService.Send(ctx, contactType, param)
driver := models.GetDriver(contactType)
err = driver.Send(param)
// err = models.NotifyService.Send(ctx, contactType, param)
if err != nil {
self.taskFailed(ctx, receiver, err.Error())
return
+2
View File
@@ -240,4 +240,6 @@ const (
ACT_PANIC = "panic"
ACT_IP_MAC_BIND = "ip_mac_bind"
// 程序内初始化notifyconfigmap错误
ACT_INIT_NOTIFY_CONFIGMAP = "init_notify_configmap"
)