From 04d930ab2bbc4bc9e2bbbd9a6941d226b2ebb360 Mon Sep 17 00:00:00 2001 From: rainzm Date: Fri, 19 Mar 2021 11:25:31 +0800 Subject: [PATCH] feat(notify): topic and subscriber at the domain level --- cmd/climc/shell/notifyv2/subscription.go | 17 +- pkg/apis/notify/const.go | 52 +- pkg/apis/notify/subscriber.go | 87 +++ pkg/apis/notify/subscription.go | 77 --- pkg/apis/notify/topic.go | 30 + pkg/apis/notify/zz_generated.model.go | 64 ++- pkg/mcclient/modules/mod_notify.go | 33 +- pkg/mcclient/options/notify/subscription.go | 86 ++- pkg/notify/models/event_template.go | 42 +- pkg/notify/models/initdb.go | 4 +- pkg/notify/models/notification.go | 95 ++-- pkg/notify/models/subscriber.go | 443 +++++++++++++++ pkg/notify/models/subscriber_receiver.go | 80 +++ pkg/notify/models/subscription.go | 577 -------------------- pkg/notify/models/subscription_receiver.go | 201 ------- pkg/notify/models/topic.go | 403 ++++++++++++++ pkg/notify/service/handlers.go | 4 +- 17 files changed, 1256 insertions(+), 1039 deletions(-) create mode 100644 pkg/apis/notify/subscriber.go delete mode 100644 pkg/apis/notify/subscription.go create mode 100644 pkg/apis/notify/topic.go create mode 100644 pkg/notify/models/subscriber.go create mode 100644 pkg/notify/models/subscriber_receiver.go delete mode 100644 pkg/notify/models/subscription.go delete mode 100644 pkg/notify/models/subscription_receiver.go create mode 100644 pkg/notify/models/topic.go diff --git a/cmd/climc/shell/notifyv2/subscription.go b/cmd/climc/shell/notifyv2/subscription.go index cecd24f30a..2e96c305f3 100644 --- a/cmd/climc/shell/notifyv2/subscription.go +++ b/cmd/climc/shell/notifyv2/subscription.go @@ -21,10 +21,15 @@ import ( ) func init() { - cmd := shell.NewResourceCmd(&modules.NotifySubscription).WithKeyword("notify-subscription") - cmd.List(new(notify.SubscriptionListOptions)) - cmd.Show(new(notify.SubscriptionOptions)) - cmd.Perform("set-receiver", new(notify.SubscriptionSetReceiverOptions)) - cmd.Perform("set-robot", new(notify.SubscriptionSetRobotOptions)) - cmd.Perform("set-webhook", new(notify.SubscriptionSetWebhookOptions)) + cmd := shell.NewResourceCmd(&modules.NotifyTopic).WithKeyword("notify-topic") + cmd.List(new(notify.TopicListOptions)) + + cmd1 := shell.NewResourceCmd(&modules.NotifySubscriber).WithKeyword("notify-subscriber") + cmd1.List(new(notify.SubscriberListOptions)) + cmd1.Create(new(notify.SubscriberCreateOptions)) + cmd1.Show(new(notify.SubscriberOptions)) + cmd1.Delete(new(notify.SubscriberOptions)) + cmd1.Perform("set-receiver", new(notify.SubscriberSetReceiverOptions)) + cmd1.Perform("enable", new(notify.SubscriberOptions)) + cmd1.Perform("disable", new(notify.SubscriberOptions)) } diff --git a/pkg/apis/notify/const.go b/pkg/apis/notify/const.go index 89bdd3acc2..6aa6fd19f0 100644 --- a/pkg/apis/notify/const.go +++ b/pkg/apis/notify/const.go @@ -84,31 +84,33 @@ const ( RECEIVER_TYPE_CONTACT = "contact" RECEIVER_TYPE_ROBOT = "robot" - SUBSCRIPTION_RESOURCE_CREATE_DELETE = "resource create delete" - SUBSCRIPTION_RESOURCE_CHANGECONFIG = "resource change config" - SUBSCRIPTION_RESOURCE_UPDATE = "resource udpate" - SUBSCRIPTION_RESOURCE_EXPIRED_RELEASE = "resource expired release" - SUBSCRIPTION_AUTOMATED_PROCESS_EXECUTION = "automated process execution" + TOPIC_TYPE_RESOURCE = "resource" + TOPIC_TYPE_AUTOMATED_PROCESS = "automated_process" - SUBSCRIPTION_TYPE_RESOURCE = "resource" - SUBSCRIPTION_TYPE_AUTOMATED_PROCESS = "automated_process" + TOPIC_RESOURCE_SERVER = "server" + TOPIC_RESOURCE_SCALINGGROUP = "scalinggroup" + TOPIC_RESOURCE_SCALINGPOLICY = "scalingpolicy" + TOPIC_RESOURCE_IMAGE = "image" + TOPIC_RESOURCE_DISK = "disk" + TOPIC_RESOURCE_SNAPSHOT = "snapshot" + TOPIC_RESOURCE_INSTANCESNAPSHOT = "instance_snapshot" + TOPIC_RESOURCE_SNAPSHOTPOLICY = "snapshotpolicy" + TOPIC_RESOURCE_NETWORK = "network" + TOPIC_RESOURCE_EIP = "eip" + TOPIC_RESOURCE_SECGROUP = "secgroup" + TOPIC_RESOURCE_LOADBALANCER = "loadbalancer" + TOPIC_RESOURCE_LOADBALANCERACL = "loadbalanceracl" + TOPIC_RESOURCE_LOADBALANCERCERTIFICATE = "loadbalancercertificate" + TOPIC_RESOURCE_BUCKET = "bucket" + TOPIC_RESOURCE_DBINSTANCE = "dbinstance" + TOPIC_RESOURCE_ELASTICCACHE = "elasticcache" + TOPIC_RESOURCE_SCHEDULEDTASK = "scheduledtask" - SUBSCRIPTION_RESOURCE_SERVER = "server" - SUBSCRIPTION_RESOURCE_SCALINGGROUP = "scalinggroup" - SUBSCRIPTION_RESOURCE_SCALINGPOLICY = "scalingpolicy" - SUBSCRIPTION_RESOURCE_IMAGE = "image" - SUBSCRIPTION_RESOURCE_DISK = "disk" - SUBSCRIPTION_RESOURCE_SNAPSHOT = "snapshot" - SUBSCRIPTION_RESOURCE_INSTANCESNAPSHOT = "instance_snapshot" - SUBSCRIPTION_RESOURCE_SNAPSHOTPOLICY = "snapshotpolicy" - SUBSCRIPTION_RESOURCE_NETWORK = "network" - SUBSCRIPTION_RESOURCE_EIP = "eip" - SUBSCRIPTION_RESOURCE_SECGROUP = "secgroup" - SUBSCRIPTION_RESOURCE_LOADBALANCER = "loadbalancer" - SUBSCRIPTION_RESOURCE_LOADBALANCERACL = "loadbalanceracl" - SUBSCRIPTION_RESOURCE_LOADBALANCERCERTIFICATE = "loadbalancercertificate" - SUBSCRIPTION_RESOURCE_BUCKET = "bucket" - SUBSCRIPTION_RESOURCE_DBINSTANCE = "dbinstance" - SUBSCRIPTION_RESOURCE_ELASTICCACHE = "elasticcache" - SUBSCRIPTION_RESOURCE_SCHEDULEDTASK = "scheduledtask" + SUBSCRIBER_TYPE_ROLE = "role" + SUBSCRIBER_TYPE_ROBOT = "robot" + SUBSCRIBER_TYPE_RECEIVER = "receiver" + + SUBSCRIBER_SCOPE_SYSTEM = "system" + SUBSCRIBER_SCOPE_DOMAIN = "domain" + SUBSCRIBER_SCOPE_PROJECT = "project" ) diff --git a/pkg/apis/notify/subscriber.go b/pkg/apis/notify/subscriber.go new file mode 100644 index 0000000000..bc3cf11834 --- /dev/null +++ b/pkg/apis/notify/subscriber.go @@ -0,0 +1,87 @@ +// 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 "yunion.io/x/onecloud/pkg/apis" + +type SubscriberCreateInput struct { + apis.VirtualResourceCreateInput + + // description: Id of Topic + // required + TopicID string + + // description: scope of resource + // enum: system,domain,project + ResourceScope string + + // description: Type of subscriber + // enum: receiver,robot,role + Type string + + // description: receivers which is required when the type is 'receiver' will Subscribe TopicID + Receivers []string + + // description: Role(Id or Name) which is required when the type is 'role' will Subscribe TopicID + Role string + + // description: The scope of role subscribers + // enum: system,domain,project + RoleScope string + + // description: Robot(Id or Name) which is required when the type is 'robot' will Subscribe TopicID + Robot string +} + +type SubscriberListInput struct { + apis.VirtualResourceListInput + apis.EnabledResourceBaseListInput + + // description: topic id + TopicID string + + // description: scope of resource + // enum: system,domain,project + ResourceScope string + + // description: type + // enum: receiver,robot,role + Type string +} + +type Identification struct { + // example: 036fed49483b412888a760c2bc995caa + ID string `json:"id"` + // example: test + Name string `json:"name"` +} + +type SubscriberDetails struct { + apis.VirtualResourceDetails + SSubscriber + + // description: receivers + Receivers []Identification + + // description: role + Role Identification + + // description: robot + Robot Identification +} + +type SubscriberSetReceiverInput struct { + Receivers []string +} diff --git a/pkg/apis/notify/subscription.go b/pkg/apis/notify/subscription.go deleted file mode 100644 index d6d7ad9808..0000000000 --- a/pkg/apis/notify/subscription.go +++ /dev/null @@ -1,77 +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 "yunion.io/x/onecloud/pkg/apis" - -type SubscriptionListInput struct { -} - -type SubscriptionDetails struct { - apis.StatusStandaloneResourceDetails - // description: resources managed - // example: ["server", "eip", "disk"] - Resources []string `json:"resource_types"` - // description: receivers of the message sent - Receivers SubscriptionReceiver `json:"receivers"` - // description: type of robot send message - // example: dingtalk_robot - Robot string `json:"robot"` - // description: webhook send message - // example: webhook - Webhook string `json:"webhook"` -} - -type IDAndName struct { - // example: 036fed49483b412888a760c2bc995caa - ID string `json:"id"` - // example: test - Name string `json:"name"` -} - -type ReceivingRoleIDAndName struct { - IDAndName - // description: scope of role - // enum: system,domain,project - Scope string `json:"scope"` -} - -type SubscriptionReceiver struct { - Receivers []IDAndName `json:"receivers"` - ReceivingRoles []ReceivingRoleIDAndName `json:"roles"` -} - -type SubscriptionSetReceiverInput struct { - ReceivingRoles []ReceivingRole `json:"roles"` - Receivers []string `json:"receivers"` -} - -type ReceivingRole struct { - // description: id or name of role - Role string `json:"role"` - // description: scope of role - // enum: system,domain,project - Scope string `json:"scope"` -} - -type SubscriptionSetRobotInput struct { - // description: robot - // enum: feishu-robot,dingtalk-robot,workwx-robot - Robot string -} - -type SubscriptionSetWebhookInput struct { - Webhook string -} diff --git a/pkg/apis/notify/topic.go b/pkg/apis/notify/topic.go new file mode 100644 index 0000000000..7fc8f3e398 --- /dev/null +++ b/pkg/apis/notify/topic.go @@ -0,0 +1,30 @@ +// 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 "yunion.io/x/onecloud/pkg/apis" + +type TopicListInput struct { + apis.StandaloneResourceListInput +} + +type TopicDetails struct { + apis.StandaloneResourceDetails + STopic + + // description: resources managed + // example: ["server", "eip", "disk"] + Resources []string `json:"resource_types"` +} diff --git a/pkg/apis/notify/zz_generated.model.go b/pkg/apis/notify/zz_generated.model.go index 7c00f13f2a..fbfbd81a7a 100644 --- a/pkg/apis/notify/zz_generated.model.go +++ b/pkg/apis/notify/zz_generated.model.go @@ -23,8 +23,10 @@ import ( // SConfig is an autogenerated struct via yunion.io/x/onecloud/pkg/notify/models.SConfig. type SConfig struct { apis.SStandaloneResourceBase - Type string `json:"type"` - Content interface{} `json:"content"` + apis.SDomainizedResourceBase + Type string `json:"type"` + Content interface{} `json:"content"` + Attribution string `json:"attribution"` } // SNotification is an autogenerated struct via yunion.io/x/onecloud/pkg/notify/models.SNotification. @@ -69,11 +71,21 @@ type SReceiverNotification struct { NotificationID string `json:"notification_id"` // ignore if ReceiverID is not empty or default Contact string `json:"contact"` + ReceiverType string `json:"receiver_type"` SendBy string `json:"send_by"` Status string `json:"status"` FailedReason string `json:"failed_reason"` } +// SRobot is an autogenerated struct via yunion.io/x/onecloud/pkg/notify/models.SRobot. +type SRobot struct { + apis.SSharableVirtualResourceBase + apis.SEnabledResourceBase + Type string `json:"type"` + Address string `json:"address"` + Lang string `json:"lang"` +} + // SSubContact is an autogenerated struct via yunion.io/x/onecloud/pkg/notify/models.SSubContact. type SSubContact struct { apis.SStandaloneResourceBase @@ -87,33 +99,31 @@ type SSubContact struct { VerifiedNote string `json:"verified_note"` } -// SSubscription is an autogenerated struct via yunion.io/x/onecloud/pkg/notify/models.SSubscription. -type SSubscription struct { - apis.SStandaloneResourceBase +// SSubscriber is an autogenerated struct via yunion.io/x/onecloud/pkg/notify/models.SSubscriber. +type SSubscriber struct { + apis.SVirtualResourceBase apis.SEnabledResourceBase - Type string `json:"type"` - Resources uint64 `json:"resources"` - Actions uint32 `json:"actions"` - AdvanceDays int `json:"advance_days"` + TopicID string `json:"topic_id"` + Type string `json:"type"` + Identification string `json:"identification"` + RoleScope string `json:"role_scope"` + ResourceScope string `json:"resource_scope"` } -// SSubscriptionReceiver is an autogenerated struct via yunion.io/x/onecloud/pkg/notify/models.SSubscriptionReceiver. -type SSubscriptionReceiver struct { - apis.SStandaloneResourceBase - SubscriptionID string `json:"subscription_id"` - // role id or receiver id or other and the value type is determined by the ReceiverType - Receiver string `json:"receiver"` - ReceiverType string `json:"receiver_type"` - RoleScope string `json:"role_scope"` -} - -// SSubscriptionReceiverDis is an autogenerated struct via yunion.io/x/onecloud/pkg/notify/models.SSubscriptionReceiverDis. -type SSubscriptionReceiverDis struct { - SSubscriptionReceiver +// SSubscriberDis is an autogenerated struct via yunion.io/x/onecloud/pkg/notify/models.SSubscriberDis. +type SSubscriberDis struct { + SSubscriber ReceiverName string `json:"receiver_name"` RoleName string `json:"role_name"` } +// SSubscriberReceiver is an autogenerated struct via yunion.io/x/onecloud/pkg/notify/models.SSubscriberReceiver. +type SSubscriberReceiver struct { + apis.SJointResourceBase + SubscriberId string `json:"subscriber_id"` + ReceiverId string `json:"receiver_id"` +} + // STemplate is an autogenerated struct via yunion.io/x/onecloud/pkg/notify/models.STemplate. type STemplate struct { apis.SStandaloneResourceBase @@ -126,6 +136,16 @@ type STemplate struct { Example string `json:"example"` } +// STopic is an autogenerated struct via yunion.io/x/onecloud/pkg/notify/models.STopic. +type STopic struct { + apis.SStandaloneResourceBase + apis.SEnabledResourceBase + Type string `json:"type"` + Resources uint64 `json:"resources"` + Actions uint32 `json:"actions"` + AdvanceDays int `json:"advance_days"` +} + // SVerification is an autogenerated struct via yunion.io/x/onecloud/pkg/notify/models.SVerification. type SVerification struct { apis.SStandaloneResourceBase diff --git a/pkg/mcclient/modules/mod_notify.go b/pkg/mcclient/modules/mod_notify.go index 75ccc1fd1b..4da4f1eb44 100644 --- a/pkg/mcclient/modules/mod_notify.go +++ b/pkg/mcclient/modules/mod_notify.go @@ -21,13 +21,14 @@ type ConfigsManager struct { } var ( - NotifyReceiver modulebase.ResourceManager - NotifyConfig modulebase.ResourceManager - NotifyRobot modulebase.ResourceManager - Notification modulebase.ResourceManager - NotifyTemplate modulebase.ResourceManager - NotifySubscription modulebase.ResourceManager - Configs ConfigsManager + NotifyReceiver modulebase.ResourceManager + NotifyConfig modulebase.ResourceManager + NotifyRobot modulebase.ResourceManager + Notification modulebase.ResourceManager + NotifyTemplate modulebase.ResourceManager + NotifyTopic modulebase.ResourceManager + NotifySubscriber modulebase.ResourceManager + Configs ConfigsManager ) func init() { @@ -71,11 +72,19 @@ func init() { ) register(&NotifyTemplate) - NotifySubscription = NewNotifyv2Manager( - "subscription", - "subscriptions", - []string{"ID", "Name", "Type", "Resource_Types", "Receivers", "Robot", "Webhook"}, + NotifyTopic = NewNotifyv2Manager( + "topic", + "topics", + []string{"ID", "Name", "Type", "Enabled", "Resources"}, []string{}, ) - register(&NotifySubscription) + register(&NotifyTopic) + + NotifySubscriber = NewNotifyv2Manager( + "subscriber", + "subscribers", + []string{"ID", "Name", "Topic_Id", "Type", "Resource_Scope", "Role_Scope", "Receivers", "Role", "Robot"}, + []string{}, + ) + register(&NotifySubscriber) } diff --git a/pkg/mcclient/options/notify/subscription.go b/pkg/mcclient/options/notify/subscription.go index 121252b211..43ee4ba84f 100644 --- a/pkg/mcclient/options/notify/subscription.go +++ b/pkg/mcclient/options/notify/subscription.go @@ -14,84 +14,76 @@ package notify import ( - "fmt" - "strings" - "yunion.io/x/jsonutils" "yunion.io/x/onecloud/pkg/mcclient/options" ) -type SubscriptionListOptions struct { +type TopicListOptions struct { options.BaseListOptions } -func (opts *SubscriptionListOptions) Params() (jsonutils.JSONObject, error) { +func (opts *TopicListOptions) Params() (jsonutils.JSONObject, error) { return options.ListStructToParams(opts) } -type SubscriptionOptions struct { - ID string `help:"Id or Name of subscription"` +type TopicOptions struct { + ID string `help:"Id or Name of topic"` } -func (so *SubscriptionOptions) GetId() string { +func (so *TopicOptions) GetId() string { return so.ID } -func (so *SubscriptionOptions) Params() (jsonutils.JSONObject, error) { +func (so *TopicOptions) Params() (jsonutils.JSONObject, error) { return nil, nil } -type SubscriptionSetReceiverOptions struct { - SubscriptionOptions - Receivers []string `json:"receivers"` - RoleAndScopes []string `json:"role_and_scopes" help:"role and scope, separated by a colon, example: admin:system"` +type SubscriberCreateOptions struct { + Name string `positional:"true"` + TopicId string `positional:"true"` + ResourceScope string `positional:"true" choices:"system|domain|project"` + Type string `positional:"true" choices:"receiver|robot|role"` + Receivers []string `help:"required if type is 'receiver'"` + Role string `help:"required if type is 'role'"` + RoleScope string `help:"required if type if 'role'"` + Robot string `help:"required if type if 'robot'"` } -func (opts *SubscriptionSetReceiverOptions) Params() (jsonutils.JSONObject, error) { - d := jsonutils.NewDict() - if len(opts.Receivers) > 0 { - d.Set("receivers", jsonutils.NewStringArray(opts.Receivers)) - } - if len(opts.RoleAndScopes) > 0 { - ra := jsonutils.NewArray() - for _, rs := range opts.RoleAndScopes { - index := strings.Index(rs, ":") - if index <= 0 { - return nil, fmt.Errorf("invalid role_and_scope %q, example: %s", rs, "admin:system") - } - rd := jsonutils.NewDict() - rd.Set("role", jsonutils.NewString(rs[:index])) - rd.Set("scope", jsonutils.NewString(rs[index+1:])) - ra.Add(rd) - } - d.Set("roles", ra) - } - return d, nil +func (sc *SubscriberCreateOptions) Params() (jsonutils.JSONObject, error) { + return jsonutils.Marshal(sc), nil } -type SsubscriptionSetRobotOptions struct { - ROBOT string `choices:"feishu-robot|dingtalk-robot|workwx-robot"` +type SubscriberListOptions struct { + options.BaseListOptions + TopicId string + ResourceScope string `choices:"system|domain|project"` + Type string `choices:"receiver|robot|role"` } -type SubscriptionSetRobotOptions struct { - SubscriptionOptions - SsubscriptionSetRobotOptions +func (sl *SubscriberListOptions) Params() (jsonutils.JSONObject, error) { + return options.ListStructToParams(sl) } -func (opts *SubscriptionSetRobotOptions) Params() (jsonutils.JSONObject, error) { - return jsonutils.Marshal(opts.SsubscriptionSetRobotOptions), nil +type SubscriberOptions struct { + ID string } -type SsubscriptionSetWebhookOptions struct { - WEBHOOK string `choices:"webhook"` +func (s *SubscriberOptions) GetId() string { + return s.ID } -type SubscriptionSetWebhookOptions struct { - SubscriptionOptions - SsubscriptionSetWebhookOptions +func (s *SubscriberOptions) Params() (jsonutils.JSONObject, error) { + return nil, nil } -func (opts *SubscriptionSetWebhookOptions) Params() (jsonutils.JSONObject, error) { - return jsonutils.Marshal(opts.SsubscriptionSetWebhookOptions), nil +type SubscriberSetReceiverOptions struct { + SubscriberOptions + Receivers []string +} + +func (ssr *SubscriberSetReceiverOptions) Params() (jsonutils.JSONObject, error) { + params := jsonutils.NewDict() + params.Set("receivers", jsonutils.NewStringArray(ssr.Receivers)) + return params, nil } diff --git a/pkg/notify/models/event_template.go b/pkg/notify/models/event_template.go index c323ad6e2e..7c95d9577c 100644 --- a/pkg/notify/models/event_template.go +++ b/pkg/notify/models/event_template.go @@ -182,12 +182,12 @@ func (lt *SLocalTemplateManager) fillWithTemplate(ctx context.Context, titleOrCo } var specFields = map[string][]string{ - notify.SUBSCRIPTION_RESOURCE_SCALINGPOLICY: { + notify.TOPIC_RESOURCE_SCALINGPOLICY: { "trigger_type", "action", "unit", }, - notify.SUBSCRIPTION_RESOURCE_SCHEDULEDTASK: { + notify.TOPIC_RESOURCE_SCHEDULEDTASK: { "resource_type", "operation", }, @@ -212,8 +212,8 @@ func init() { stI18nTable.Set(comapi.ST_RESOURCE_OPERATION_STOP, i18n.NewTableEntry().EN("stop").CN("关机")) stI18nTable.Set(comapi.ST_RESOURCE_OPERATION_START, i18n.NewTableEntry().EN("start").CN("开机")) - specFieldTrans[notify.SUBSCRIPTION_RESOURCE_SCALINGPOLICY] = spI18nTable - specFieldTrans[notify.SUBSCRIPTION_RESOURCE_SCHEDULEDTASK] = stI18nTable + specFieldTrans[notify.TOPIC_RESOURCE_SCALINGPOLICY] = spI18nTable + specFieldTrans[notify.TOPIC_RESOURCE_SCHEDULEDTASK] = stI18nTable } func (lt *SLocalTemplateManager) getTemplate(ctx context.Context, titleOrContent string, contactType string, topic string, lang string) (*template.Template, error) { @@ -307,87 +307,87 @@ func init() { api.TEMPLATE_LANG_CN, }, sI18nElme{ - api.SUBSCRIPTION_RESOURCE_SERVER, + api.TOPIC_RESOURCE_SERVER, "virtual machine", "虚拟机", }, sI18nElme{ - api.SUBSCRIPTION_RESOURCE_SCALINGGROUP, + api.TOPIC_RESOURCE_SCALINGGROUP, "scaling group", "弹性伸缩组", }, sI18nElme{ - api.SUBSCRIPTION_RESOURCE_SCALINGPOLICY, + api.TOPIC_RESOURCE_SCALINGPOLICY, "scaling policy", "弹性伸缩策略", }, sI18nElme{ - api.SUBSCRIPTION_RESOURCE_IMAGE, + api.TOPIC_RESOURCE_IMAGE, "image", "系统镜像", }, sI18nElme{ - api.SUBSCRIPTION_RESOURCE_DISK, + api.TOPIC_RESOURCE_DISK, "disk", "硬盘", }, sI18nElme{ - api.SUBSCRIPTION_RESOURCE_SNAPSHOT, + api.TOPIC_RESOURCE_SNAPSHOT, "snapshot", "硬盘快照", }, sI18nElme{ - api.SUBSCRIPTION_RESOURCE_INSTANCESNAPSHOT, + api.TOPIC_RESOURCE_INSTANCESNAPSHOT, "instance snapshot", "主机快照", }, sI18nElme{ - api.SUBSCRIPTION_RESOURCE_NETWORK, + api.TOPIC_RESOURCE_NETWORK, "network", "IP子网", }, sI18nElme{ - api.SUBSCRIPTION_RESOURCE_EIP, + api.TOPIC_RESOURCE_EIP, "EIP", "弹性公网IP", }, sI18nElme{ - api.SUBSCRIPTION_RESOURCE_SECGROUP, + api.TOPIC_RESOURCE_SECGROUP, "security group", "安全组", }, sI18nElme{ - api.SUBSCRIPTION_RESOURCE_LOADBALANCER, + api.TOPIC_RESOURCE_LOADBALANCER, "loadbalancer instance", "负载均衡实例", }, sI18nElme{ - api.SUBSCRIPTION_RESOURCE_LOADBALANCERACL, + api.TOPIC_RESOURCE_LOADBALANCERACL, "loadbalancer ACL", "负载均衡访问控制", }, sI18nElme{ - api.SUBSCRIPTION_RESOURCE_LOADBALANCERCERTIFICATE, + api.TOPIC_RESOURCE_LOADBALANCERCERTIFICATE, "loadbalancer certificate", "负载均衡证书", }, sI18nElme{ - api.SUBSCRIPTION_RESOURCE_BUCKET, + api.TOPIC_RESOURCE_BUCKET, "object storage bucket", "对象存储桶", }, sI18nElme{ - api.SUBSCRIPTION_RESOURCE_DBINSTANCE, + api.TOPIC_RESOURCE_DBINSTANCE, "RDS instance", "RDS实例", }, sI18nElme{ - api.SUBSCRIPTION_RESOURCE_ELASTICCACHE, + api.TOPIC_RESOURCE_ELASTICCACHE, "Redis instance", "Redis实例", }, sI18nElme{ - api.SUBSCRIPTION_RESOURCE_SCHEDULEDTASK, + api.TOPIC_RESOURCE_SCHEDULEDTASK, "scheduled task", "定时任务", }, diff --git a/pkg/notify/models/initdb.go b/pkg/notify/models/initdb.go index a5116a6f85..0dbaca87b7 100644 --- a/pkg/notify/models/initdb.go +++ b/pkg/notify/models/initdb.go @@ -32,8 +32,8 @@ func InitDB() error { ConfigManager, TemplateManager, ReceiverNotificationManager, - SubscriptionManager, - RobotManager, + TopicManager, + RobotManager, } { err := manager.InitializeData() if err != nil { diff --git a/pkg/notify/models/notification.go b/pkg/notify/models/notification.go index a1ebc1d091..a0c1d99e72 100644 --- a/pkg/notify/models/notification.go +++ b/pkg/notify/models/notification.go @@ -232,16 +232,16 @@ func (nm *SNotificationManager) PerformEventNotify(ctx context.Context, userCred } // receiver - subscriptions, err := SubscriptionManager.SubsciptionByEvent(input.Event, input.AdvanceDays) + topics, err := TopicManager.TopicsByEvent(input.Event, input.AdvanceDays) if err != nil { return output, errors.Wrapf(err, "unable fetch subscriptions by event %q", input.Event) } - if len(subscriptions) == 0 { + if len(topics) == 0 { return output, nil } var receiverIds []string - for i := range subscriptions { - receiverIds1, err := SubscriptionReceiverManager.getReceivers(ctx, subscriptions[i].Id, input.ProjectDomainId, input.ProjectId) + for i := range topics { + receiverIds1, err := SubscriberManager.getReceiversSent(ctx, topics[i].Id, input.ProjectDomainId, input.ProjectId) if err != nil { return output, errors.Wrap(err, "unable to get receive") } @@ -252,14 +252,14 @@ func (nm *SNotificationManager) PerformEventNotify(ctx context.Context, userCred // robot var robots []string - for i := range subscriptions { - robot, err := SubscriptionReceiverManager.robot(subscriptions[i].Id) + for i := range topics { + _robots, err := SubscriberManager.robot(topics[i].Id, input.ProjectDomainId, input.ProjectId) if err != nil { if errors.Cause(err) != errors.ErrNotFound { - return output, errors.Wrapf(err, "unable fetch robot of subscription %q", subscriptions[i].Id) + return output, errors.Wrapf(err, "unable fetch robot of subscription %q", topics[i].Id) } } else { - robots = append(robots, robot) + robots = append(robots, _robots...) } } robots = intersection(robots, intersection(cts, RobotContactTypes)) @@ -267,22 +267,6 @@ func (nm *SNotificationManager) PerformEventNotify(ctx context.Context, userCred robots = sets.NewString(robots...).UnsortedList() } - // webhook - var webhooks []string - for i := range subscriptions { - webhook, err := SubscriptionReceiverManager.webhook(subscriptions[i].Id) - if err != nil { - if errors.Cause(err) != errors.ErrNotFound { - return output, errors.Wrapf(err, "unable to fetch webhook of subscription %q", subscriptions[i].Id) - } - } else { - webhooks = append(webhooks, webhook) - } - } - if len(webhooks) > 0 { - webhooks = sets.NewString(webhooks...).UnsortedList() - } - message := jsonutils.Marshal(input.ResourceDetails).String() // fillter non-existed receiver @@ -313,39 +297,56 @@ func (nm *SNotificationManager) PerformEventNotify(ctx context.Context, userCred } // normal contact type for _, ct := range contactTypes { - err := nm.create(ctx, userCred, ct, receiverIds, []string{}, input.Priority, "", message, input.Event, input.AdvanceDays) + err := nm.create(ctx, userCred, ct, receiverIds, nil, input.Priority, "", message, input.Event, input.AdvanceDays) if err != nil { output.FailedList = append(output.FailedList, api.FailedElem{ ContactType: ct, Reason: err.Error(), }) } - } // robot - for _, robot := range robots { - err := nm.create(ctx, userCred, robot, []string{}, []string{}, input.Priority, "", message, input.Event, input.AdvanceDays) - if err != nil { - output.FailedList = append(output.FailedList, api.FailedElem{ - ContactType: robot, - Reason: err.Error(), - }) - } - } - // webhook - for _, webhook := range webhooks { - err := nm.create(ctx, userCred, webhook, []string{}, []string{}, input.Priority, "", message, input.Event, input.AdvanceDays) - if err != nil { - output.FailedList = append(output.FailedList, api.FailedElem{ - ContactType: webhook, - Reason: err.Error(), - }) - } - } - + err = nm.createWithRobots(ctx, userCred, robots, input.Priority, "", message, input.Event, input.AdvanceDays) + output.FailedList = append(output.FailedList, api.FailedElem{ + ContactType: api.ROBOT, + Reason: err.Error(), + }) return output, nil } +func (nm *SNotificationManager) createWithRobots(ctx context.Context, userCred mcclient.TokenCredential, robotIds []string, priority, topic, message, event string, advanceDays int) error { + if len(robotIds) == 0 { + return nil + } + n := &SNotification{ + ContactType: api.ROBOT, + Message: message, + Priority: priority, + ReceivedAt: time.Now(), + Event: event, + AdvanceDays: advanceDays, + } + 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, topic, message, event string, advanceDays int) error { if len(receiverIds)+len(contacts) == 0 { log.Infof("%s: no send", contactType) @@ -374,7 +375,7 @@ func (nm *SNotificationManager) create(ctx context.Context, userCred mcclient.To for i := range contacts { _, err := ReceiverNotificationManager.CreateContact(ctx, userCred, contacts[i], n.Id) if err != nil { - return errors.Wrap(err, "ReceiverNotificationManager.Create") + return errors.Wrap(err, "ReceiverNotificationManager.CreateContact") } } log.Infof("start NotificationSendTask for %s", contactType) diff --git a/pkg/notify/models/subscriber.go b/pkg/notify/models/subscriber.go new file mode 100644 index 0000000000..4ffc7a5151 --- /dev/null +++ b/pkg/notify/models/subscriber.go @@ -0,0 +1,443 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package models + +import ( + "context" + "database/sql" + "fmt" + "sort" + "strings" + + "golang.org/x/sync/errgroup" + "k8s.io/apimachinery/pkg/util/sets" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/utils" + "yunion.io/x/sqlchemy" + + 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" + "yunion.io/x/onecloud/pkg/mcclient/auth" + "yunion.io/x/onecloud/pkg/mcclient/modules" + "yunion.io/x/onecloud/pkg/util/stringutils2" +) + +var SubscriberManager *SSubscriberManager + +func init() { + SubscriberManager = &SSubscriberManager{ + SVirtualResourceBaseManager: db.NewVirtualResourceBaseManager( + SSubscriber{}, + "subscriber_tbl", + "subscriber", + "subscribers", + ), + } + SubscriberManager.SetVirtualObject(ReceiverNotificationManager) +} + +type SSubscriberManager struct { + db.SVirtualResourceBaseManager + db.SEnabledResourceBaseManager +} + +type SSubscriber struct { + db.SVirtualResourceBase + db.SEnabledResourceBase + + TopicID string `width:"128" charset:"ascii" nullable:"false" index:"true" get:"user" list:"user"` + Type string `width:"16" charset:"ascii" nullable:"false" index:"true" get:"user" list:"user"` + Identification string `width:"128" charset:"ascii" nullable:"false"` + RoleScope string `width:"8" charset:"ascii" nullable:"false" get:"user" list:"user"` + ResourceScope string `width:"8" charset:"ascii" nullable:"false" get:"user" list:"user"` +} + +func (sm *SSubscriberManager) validateReceivers(ctx context.Context, receivers []string) ([]string, error) { + rs, err := ReceiverManager.FetchByIdOrNames(ctx, receivers...) + if err != nil { + return nil, errors.Wrap(err, "unable to fetch Receivers") + } + reSet := sets.NewString(receivers...) + reIds := make([]string, len(rs)) + for i := range rs { + reSet.Delete(rs[i].GetId()) + reSet.Delete(rs[i].GetName()) + reIds[i] = rs[i].GetId() + } + if reSet.Len() > 0 { + return nil, httperrors.NewInputParameterError("receivers %q not found", strings.Join(reSet.UnsortedList(), ", ")) + } + return reIds, 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 + input.VirtualResourceCreateInput, err = sm.SVirtualResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, input.VirtualResourceCreateInput) + if err != nil { + return input, errors.Wrap(err, "SVirtualResourceBaseManager.ValidateCreateData") + } + // check topic + t, err := TopicManager.FetchById(input.TopicID) + if err != nil { + return input, errors.Wrapf(err, "unable to fetch topic %s", input.TopicID) + } + // check resource scope + if !utils.IsInStringArray(input.ResourceScope, []string{api.SUBSCRIBER_SCOPE_SYSTEM, api.SUBSCRIBER_SCOPE_DOMAIN, api.SUBSCRIBER_SCOPE_PROJECT}) { + return input, httperrors.NewInputParameterError("unknown resource_scope %q", input.ResourceScope) + } + input.TopicID = t.GetId() + switch input.Type { + case api.SUBSCRIBER_TYPE_RECEIVER: + reIds, err := sm.validateReceivers(ctx, input.Receivers) + if err != nil { + return input, err + } + input.Receivers = reIds + case api.SUBSCRIBER_TYPE_ROLE: + if input.RoleScope == "" { + input.RoleScope = input.ResourceScope + } + roleCache, err := db.RoleCacheManager.FetchRoleByIdOrName(ctx, input.Role) + if err != nil { + return input, errors.Wrapf(err, "unable find role %q", input.Role) + } + input.Role = roleCache.GetId() + case api.SUBSCRIBER_TYPE_ROBOT: + robot, err := RobotManager.FetchByIdOrName(userCred, input.Robot) + if errors.Cause(err) == sql.ErrNoRows { + return input, httperrors.NewInputParameterError("robot %q not found", input.Robot) + } + if err != nil { + return input, errors.Wrapf(err, "unable to fetch robot %q", input.Robot) + } + input.Robot = robot.GetId() + default: + return input, httperrors.NewInputParameterError("unkown type %q", input.Type) + } + return input, nil +} + +func (s *SSubscriber) CustomizeCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) error { + err := s.SVirtualResourceBase.CustomizeCreate(ctx, userCred, ownerId, query, data) + if err != nil { + return errors.Wrap(err, "SVirtualResourceBase.CustomizeCreate") + } + var input api.SubscriberCreateInput + _ = data.Unmarshal(&input) + switch input.Type { + case api.SUBSCRIBER_TYPE_RECEIVER: + err := s.SetReceivers(ctx, input.Receivers) + if err != nil { + return errors.Wrapf(err, "unable to set connect receivers with subscriber %s", s.Id) + } + case api.SUBSCRIBER_TYPE_ROBOT: + s.Identification = input.Robot + case api.SUBSCRIBER_TYPE_ROLE: + s.Identification = input.Role + } + return nil +} + +func (sm *SSubscriberManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, input api.SubscriberListInput) (*sqlchemy.SQuery, error) { + var err error + q, err = sm.SVirtualResourceBaseManager.ListItemFilter(ctx, q, userCred, input.VirtualResourceListInput) + if err != nil { + return nil, errors.Wrap(err, "SVirtualResourceBaseManager.ListItemFilter") + } + q, err = sm.SEnabledResourceBaseManager.ListItemFilter(ctx, q, userCred, input.EnabledResourceBaseListInput) + if err != nil { + return nil, errors.Wrap(err, "SEnabledResourceBaseManager.ListItemFilter") + } + if input.TopicID != "" { + q = q.Equals("topic_id", input.TopicID) + } + if input.Type != "" { + q = q.Equals("type", input.Type) + } + if input.ResourceScope != "" { + q = q.Equals("resource_scope", input.ResourceScope) + } + return q, nil +} + +func (sm *SSubscriberManager) FetchCustomizeColumns(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, objs []interface{}, fields stringutils2.SSortedStrings, isList bool) []api.SubscriberDetails { + var err error + vRows := sm.SVirtualResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList) + rows := make([]api.SubscriberDetails, len(objs)) + for i := range rows { + rows[i].VirtualResourceDetails = vRows[i] + s := objs[i].(*SSubscriber) + switch s.Type { + case api.SUBSCRIBER_TYPE_RECEIVER: + rows[i].Receivers, err = s.receiverIdentifications() + if err != nil { + log.Errorf("unable to get receiverIdentifications for subscriber %q: %v", s.Id, err) + } + case api.SUBSCRIBER_TYPE_ROBOT: + rows[i].Robot, err = s.robotIdentification() + if err != nil { + log.Errorf("unable get robotIdentification for subscriber %q: %v", s.Id, err) + } + case api.SUBSCRIBER_TYPE_ROLE: + rows[i].Role, err = s.roleIdentification() + if err != nil { + log.Errorf("unable to get roleIdentification for subscriber %q: %v", s.Id, err) + } + } + } + return rows +} + +func (s *SSubscriber) receiverIdentifications() ([]api.Identification, error) { + srSubq := SubscriberReceiverManager.Query().Equals("subscription_id", s.Id).SubQuery() + rq := ReceiverManager.Query("id", "name") + rq = rq.Join(srSubq, sqlchemy.Equals(srSubq.Field("receiver_id"), rq.Field("id"))) + var ret []api.Identification + err := rq.All(&ret) + if err != nil { + return nil, err + } + return ret, nil +} + +func (s *SSubscriber) robotIdentification() (api.Identification, error) { + var ret api.Identification + q := RobotManager.Query("id", "name").Equals("id", s.Identification) + err := q.First(&ret) + if err != nil { + return ret, err + } + return ret, nil +} + +func (s *SSubscriber) roleIdentification() (api.Identification, error) { + var ret api.Identification + q := db.RoleCacheManager.Query("id", "name").Equals("id", s.Identification) + err := q.First(&ret) + if err != nil { + return ret, err + } + return ret, nil +} + +func (srm *SSubscriberManager) robot(tid, projectDomainId, projectId string) ([]string, error) { + srs, err := srm.findSuitableOnes(tid, projectDomainId, projectId, api.SUBSCRIBER_TYPE_ROBOT) + if err != nil { + return nil, err + } + robotIds := make([]string, len(srs)) + for i := range srs { + robotIds[i] = srs[i].Identification + } + return robotIds, nil +} + +func (srm *SSubscriberManager) findSuitableOnes(tid, projectDomainId, projectId string, types ...string) ([]SSubscriber, error) { + q := srm.Query().Equals("subscription_id", tid) + q = q.Filter(sqlchemy.OR( + sqlchemy.AND( + sqlchemy.Equals(q.Field("resource_type"), api.SUBSCRIBER_SCOPE_PROJECT), + sqlchemy.Equals(q.Field("project_id"), projectId), + ), + sqlchemy.AND( + sqlchemy.Equals(q.Field("resource_type"), api.SUBSCRIBER_SCOPE_DOMAIN), + sqlchemy.Equals(q.Field("domain_id"), projectDomainId), + ), + sqlchemy.Equals(q.Field("resource_type"), api.SUBSCRIBER_SCOPE_SYSTEM), + )) + switch len(types) { + case 0: + case 1: + q = q.Equals("type", types[0]) + default: + q = q.In("type", types) + } + srs := make([]SSubscriber, 0, 1) + err := db.FetchModelObjects(srm, q, &srs) + if err != nil { + return nil, err + } + return srs, nil +} + +// TODO: Use cache to increase speed +func (srm *SSubscriberManager) getReceiversSent(ctx context.Context, tid string, projectDomainId string, projectId string) ([]string, error) { + srs, err := srm.findSuitableOnes(tid, projectDomainId, projectId, api.SUBSCRIBER_TYPE_RECEIVER, api.SUBSCRIBER_TYPE_ROLE) + if err != nil { + return nil, err + } + receivers := make([]string, 0, len(srs)) + roleMap := make(map[string][]string, 3) + receivermap := make(map[string]*[]string, 3) + for _, sr := range srs { + if sr.Type == api.SUBSCRIBER_TYPE_RECEIVER { + rIds, err := sr.getReceivers() + if err != nil { + return nil, errors.Wrap(err, "unable to get receivers") + } + receivers = append(receivers, rIds...) + } else if sr.Type == api.SUBSCRIBER_TYPE_ROBOT { + roleMap[sr.RoleScope] = append(roleMap[sr.RoleScope], sr.Identification) + receivermap[sr.RoleScope] = &[]string{} + } + } + errgo, _ := errgroup.WithContext(ctx) + for _scope, _roles := range roleMap { + scope, roles := _scope, _roles + receivers := receivermap[scope] + errgo.Go(func() error { + query := jsonutils.NewDict() + query.Set("roles", jsonutils.NewStringArray(roles)) + query.Set("effective", jsonutils.JSONTrue) + switch scope { + case api.SUBSCRIBER_SCOPE_SYSTEM: + case api.SUBSCRIBER_SCOPE_DOMAIN: + if len(projectDomainId) == 0 { + return fmt.Errorf("need projectDomainId") + } + query.Set("project_domain_id", jsonutils.NewString(projectDomainId)) + case api.SUBSCRIBER_SCOPE_PROJECT: + if len(projectId) == 0 { + return fmt.Errorf("need projectId") + } + query.Add(jsonutils.NewString(projectId), "scope", "project", "id") + } + s := auth.GetAdminSession(ctx, "", "") + log.Debugf("query for role-assignments: %s", query.String()) + listRet, err := modules.RoleAssignments.List(s, query) + if err != nil { + return errors.Wrap(err, "unable to list RoleAssignments") + } + log.Debugf("return value for role-assignments: %s", jsonutils.Marshal(listRet)) + for i := range listRet.Data { + ras := listRet.Data[i] + user, err := ras.Get("user") + if err == nil { + id, err := user.GetString("id") + if err != nil { + return errors.Wrap(err, "unable to get user.id from result of RoleAssignments.List") + } + *receivers = append(*receivers, id) + } + } + return nil + }) + } + err = errgo.Wait() + if err != nil { + return nil, err + } + for _, res := range receivermap { + receivers = append(receivers, *res...) + } + // de-duplication + return sets.NewString(receivers...).UnsortedList(), nil +} + +func (sr *SSubscriber) getReceivers() ([]string, error) { + srrs, err := SubscriberReceiverManager.getBySubscriberId(sr.Id) + if err != nil { + return nil, err + } + rIds := make([]string, len(srrs)) + for i := range srrs { + rIds[i] = srrs[i].ReceiverId + } + return rIds, nil +} + +func (sr *SSubscriber) SetReceivers(ctx context.Context, receiverIds []string) error { + srrs, err := SubscriberReceiverManager.getBySubscriberId(sr.Id) + if err != nil { + return errors.Wrapf(err, "unable to get SRReceiver by Subscriber %s", sr.Id) + } + dbReceivers := make([]string, len(srrs)) + for i := range srrs { + dbReceivers[i] = srrs[i].ReceiverId + } + var addReceivers, rmReceivers []string + sort.Strings(dbReceivers) + sort.Strings(receiverIds) + for i, j := 0, 0; i < len(dbReceivers) || j < len(receiverIds); { + switch { + case i == len(dbReceivers): + addReceivers = append(addReceivers, receiverIds[j]) + j++ + case j == len(receiverIds): + rmReceivers = append(rmReceivers, dbReceivers[i]) + i++ + case dbReceivers[i] > receiverIds[j]: + rmReceivers = append(rmReceivers, dbReceivers[i]) + i++ + case dbReceivers[i] < receiverIds[j]: + addReceivers = append(addReceivers, receiverIds[j]) + j++ + case dbReceivers[i] == receiverIds[j]: + } + } + // add + for _, rId := range addReceivers { + _, err := SubscriberReceiverManager.create(ctx, sr.Id, rId) + if err != nil { + return errors.Wrapf(err, "unable to connect subscription receiver %q with receiver %q", sr.Id, rId) + } + } + for _, rId := range rmReceivers { + err := SubscriberReceiverManager.delete(sr.Id, rId) + if err != nil { + return errors.Wrapf(err, "unable to disconnect subscription receiver %q with receiver %q", sr.Id, rId) + } + } + return nil +} + +func (s *SSubscriber) AllowPerformSetReceiver(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) bool { + return s.IsOwner(userCred) || db.IsAdminAllowPerform(userCred, s, "set-receiver") +} + +func (s *SSubscriber) PerformSetReceiver(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.SubscriberSetReceiverInput) (jsonutils.JSONObject, error) { + reIds, err := SubscriberManager.validateReceivers(ctx, input.Receivers) + if err != nil { + return nil, err + } + return nil, s.SetReceivers(ctx, reIds) +} + +func (s *SSubscriber) AllowPerformEnable(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input jsonutils.JSONObject) bool { + return db.IsAdminAllowPerform(userCred, s, "enable") +} + +func (s *SSubscriber) PerformEnable(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input jsonutils.JSONObject) (jsonutils.JSONObject, error) { + err := db.EnabledPerformEnable(s, ctx, userCred, true) + if err != nil { + return nil, errors.Wrap(err, "EnabledPerformEnable") + } + return nil, nil +} + +func (s *SSubscriber) AllowPerformDisable(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input jsonutils.JSONObject) bool { + return db.IsAdminAllowPerform(userCred, s, "disable") +} + +func (s *SSubscriber) PerformDisable(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input jsonutils.JSONObject) (jsonutils.JSONObject, error) { + err := db.EnabledPerformEnable(s, ctx, userCred, false) + if err != nil { + return nil, errors.Wrap(err, "EnabledPerformEnable") + } + return nil, nil +} diff --git a/pkg/notify/models/subscriber_receiver.go b/pkg/notify/models/subscriber_receiver.go new file mode 100644 index 0000000000..f7927b48fe --- /dev/null +++ b/pkg/notify/models/subscriber_receiver.go @@ -0,0 +1,80 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package models + +import ( + "context" + + "yunion.io/x/onecloud/pkg/cloudcommon/db" +) + +var SubscriberReceiverManager *SSubscriberReceiverManager + +func init() { + SubscriberReceiverManager = &SSubscriberReceiverManager{ + SJointResourceBaseManager: db.NewJointResourceBaseManager( + SSubscriberReceiver{}, + "subscriber_receiver_tbl", + "subscriber_receiver", + "subscriber_receivers", + SubscriberManager, + ReceiverManager, + ), + } + SubscriberReceiverManager.SetVirtualObject(SubscriberReceiverManager) +} + +type SSubscriberReceiverManager struct { + db.SJointResourceBaseManager +} + +type SSubscriberReceiver struct { + db.SJointResourceBase + SubscriberId string `width:"36" charset:"ascii" nullable:"false" index:"true"` + ReceiverId string `width:"36" charset:"ascii" nullable:"false" index:"true"` +} + +func (srm *SSubscriberReceiverManager) getBySubscriberId(sId string) ([]SSubscriberReceiver, error) { + q := srm.Query().Equals("subscriber_id", sId) + srs := make([]SSubscriberReceiver, 0, 2) + err := db.FetchModelObjects(srm, q, &srs) + if err != nil { + return nil, err + } + return srs, nil +} + +func (srm *SSubscriberReceiverManager) create(ctx context.Context, sId, receiverId string) (*SSubscriberReceiver, error) { + sr := &SSubscriberReceiver{ + SubscriberId: sId, + ReceiverId: receiverId, + } + return sr, srm.TableSpec().Insert(ctx, sr) +} + +func (srm *SSubscriberReceiverManager) delete(sId, receiverId string) error { + q := srm.Query().Equals("receiver_id", receiverId) + var sr SSubscriberReceiver + err := q.First(&sr) + if err != nil { + return err + } + srp := &sr + _, err = db.Update(srp, func() error { + srp.MarkDelete() + return nil + }) + return err +} diff --git a/pkg/notify/models/subscription.go b/pkg/notify/models/subscription.go deleted file mode 100644 index 876475e08a..0000000000 --- a/pkg/notify/models/subscription.go +++ /dev/null @@ -1,577 +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 models - -import ( - "context" - "fmt" - "strings" - - "golang.org/x/sync/errgroup" - - "yunion.io/x/jsonutils" - "yunion.io/x/log" - "yunion.io/x/pkg/errors" - "yunion.io/x/pkg/util/sets" - "yunion.io/x/pkg/utils" - "yunion.io/x/sqlchemy" - - "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" - "yunion.io/x/onecloud/pkg/util/bitmap" - "yunion.io/x/onecloud/pkg/util/rbacutils" - "yunion.io/x/onecloud/pkg/util/stringutils2" -) - -func parseEvent(es string) (notify.SEvent, error) { - ess := strings.Split(es, notify.DelimiterInEvent) - if len(ess) != 2 { - return notify.SEvent{}, fmt.Errorf("invalid event string %q", es) - } - return notify.Event.WithResourceType(ess[0]).WithAction(notify.SAction(ess[1])), nil -} - -type SSubscriptionManager struct { - db.SStandaloneResourceBaseManager - db.SEnabledResourceBaseManager -} - -var SubscriptionManager *SSubscriptionManager - -func init() { - SubscriptionManager = &SSubscriptionManager{ - SStandaloneResourceBaseManager: db.NewStandaloneResourceBaseManager( - SSubscription{}, - "subscription_tbl", - "subscription", - "subscriptions", - ), - } - SubscriptionManager.SetVirtualObject(SubscriptionManager) -} - -type SSubscription struct { - db.SStandaloneResourceBase - db.SEnabledResourceBase - - Type string `width:"20" nullable:"false" create:"required" update:"user" list:"user"` - Resources uint64 `nullable:"false"` - Actions uint32 `nullable:"false"` - AdvanceDays int `nullable:"false"` -} - -const ( - DefaultResourceCreateDelete = "resource create or delete" - DefaultResourceChangeConfig = "resource change config" - DefaultResourceUpdate = "resource update" - DefaultResourceReleaseDue1Day = "resource release due 1 day" - DefaultResourceReleaseDue3Day = "resource release due 3 day" - DefaultScheduledTaskExecute = "scheduled task execute" - DefaultScalingPolicyExecute = "scaling policy execute" - DefaultSnapshotPolicyExecute = "snapshot policy execute" -) - -func (sm *SSubscriptionManager) InitializeData() error { - initSNames := sets.NewString( - DefaultResourceCreateDelete, - DefaultResourceChangeConfig, - DefaultResourceUpdate, - DefaultResourceReleaseDue1Day, - DefaultResourceReleaseDue3Day, - DefaultScheduledTaskExecute, - DefaultScalingPolicyExecute, - DefaultSnapshotPolicyExecute, - ) - q := sm.Query() - subscriptions := make([]SSubscription, 0, initSNames.Len()) - err := db.FetchModelObjects(sm, q, &subscriptions) - if err != nil { - return errors.Wrap(err, "unable to FetchModelObjects") - } - for i := range subscriptions { - ss := &subscriptions[i] - initSNames.Delete(ss.Name) - } - ctx := context.Background() - for _, name := range initSNames.UnsortedList() { - ss := new(SSubscription) - ss.Name = name - switch name { - case DefaultResourceCreateDelete: - ss.addResources( - notify.SUBSCRIPTION_RESOURCE_SERVER, - notify.SUBSCRIPTION_RESOURCE_SCALINGGROUP, - notify.SUBSCRIPTION_RESOURCE_IMAGE, - notify.SUBSCRIPTION_RESOURCE_DISK, - notify.SUBSCRIPTION_RESOURCE_SNAPSHOT, - notify.SUBSCRIPTION_RESOURCE_INSTANCESNAPSHOT, - notify.SUBSCRIPTION_RESOURCE_SNAPSHOTPOLICY, - notify.SUBSCRIPTION_RESOURCE_NETWORK, - notify.SUBSCRIPTION_RESOURCE_EIP, - notify.SUBSCRIPTION_RESOURCE_LOADBALANCER, - notify.SUBSCRIPTION_RESOURCE_LOADBALANCERACL, - notify.SUBSCRIPTION_RESOURCE_LOADBALANCERCERTIFICATE, - notify.SUBSCRIPTION_RESOURCE_BUCKET, - notify.SUBSCRIPTION_RESOURCE_DBINSTANCE, - notify.SUBSCRIPTION_RESOURCE_ELASTICCACHE, - ) - ss.addAction( - notify.ActionCreate, - notify.ActionDelete, - notify.ActionPendingDelete, - ) - ss.Type = notify.SUBSCRIPTION_TYPE_RESOURCE - case DefaultResourceChangeConfig: - ss.addResources( - notify.SUBSCRIPTION_RESOURCE_SERVER, - notify.SUBSCRIPTION_RESOURCE_DISK, - notify.SUBSCRIPTION_RESOURCE_DBINSTANCE, - notify.SUBSCRIPTION_RESOURCE_ELASTICCACHE, - ) - ss.addAction(notify.ActionChangeConfig) - ss.Type = notify.SUBSCRIPTION_TYPE_RESOURCE - case DefaultResourceUpdate: - ss.addResources( - notify.SUBSCRIPTION_RESOURCE_SERVER, - notify.SUBSCRIPTION_RESOURCE_DISK, - notify.SUBSCRIPTION_RESOURCE_DBINSTANCE, - notify.SUBSCRIPTION_RESOURCE_ELASTICCACHE, - ) - ss.addAction(notify.ActionUpdate) - ss.Type = notify.SUBSCRIPTION_TYPE_RESOURCE - case DefaultResourceReleaseDue1Day: - ss.addResources( - notify.SUBSCRIPTION_RESOURCE_SERVER, - notify.SUBSCRIPTION_RESOURCE_DISK, - notify.SUBSCRIPTION_RESOURCE_EIP, - notify.SUBSCRIPTION_RESOURCE_LOADBALANCER, - notify.SUBSCRIPTION_RESOURCE_DBINSTANCE, - notify.SUBSCRIPTION_RESOURCE_ELASTICCACHE, - ) - ss.addAction(notify.ActionExpiredRelease) - ss.Type = notify.SUBSCRIPTION_TYPE_RESOURCE - ss.AdvanceDays = 1 - case DefaultResourceReleaseDue3Day: - ss.addResources( - notify.SUBSCRIPTION_RESOURCE_SERVER, - notify.SUBSCRIPTION_RESOURCE_DISK, - notify.SUBSCRIPTION_RESOURCE_EIP, - notify.SUBSCRIPTION_RESOURCE_LOADBALANCER, - notify.SUBSCRIPTION_RESOURCE_DBINSTANCE, - notify.SUBSCRIPTION_RESOURCE_ELASTICCACHE, - ) - ss.addAction(notify.ActionExpiredRelease) - ss.Type = notify.SUBSCRIPTION_TYPE_RESOURCE - ss.AdvanceDays = 3 - case DefaultScheduledTaskExecute: - ss.addResources(notify.SUBSCRIPTION_RESOURCE_SCHEDULEDTASK) - ss.addAction(notify.ActionExecute) - ss.Type = notify.SUBSCRIPTION_TYPE_AUTOMATED_PROCESS - case DefaultScalingPolicyExecute: - ss.addResources(notify.SUBSCRIPTION_RESOURCE_SCALINGPOLICY) - ss.addAction(notify.ActionExecute) - ss.Type = notify.SUBSCRIPTION_TYPE_AUTOMATED_PROCESS - case DefaultSnapshotPolicyExecute: - ss.addResources(notify.SUBSCRIPTION_RESOURCE_SNAPSHOTPOLICY) - ss.addAction(notify.ActionExecute) - ss.Type = notify.SUBSCRIPTION_TYPE_AUTOMATED_PROCESS - } - err := sm.TableSpec().Insert(ctx, ss) - if err != nil { - return errors.Wrapf(err, "unable to insert %s", name) - } - } - return nil -} - -func (sm *SSubscriptionManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, input notify.SubscriptionListInput) (*sqlchemy.SQuery, error) { - return q, nil -} - -func (sm *SSubscriptionManager) FetchCustomizeColumns(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, objs []interface{}, fields stringutils2.SSortedStrings, isList bool) []notify.SubscriptionDetails { - sRows := sm.SStandaloneResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList) - rows := make([]notify.SubscriptionDetails, len(objs)) - for i := range rows { - rows[i].StandaloneResourceDetails = sRows[i] - ss := objs[i].(*SSubscription) - rows[i].Resources = ss.getResources() - srs, err := ss.subscriptionReceiverDiss() - if err != nil { - log.Errorf("unable to get subscriptionReceivers: %v", err) - } - for j := range srs { - sr := &srs[j] - switch sr.ReceiverType { - case ReceiverNormal: - rows[i].Receivers.Receivers = append(rows[i].Receivers.Receivers, notify.IDAndName{ - ID: sr.Receiver, - Name: sr.ReceiverName, - }) - case ReceiverRole: - rows[i].Receivers.ReceivingRoles = append(rows[i].Receivers.ReceivingRoles, notify.ReceivingRoleIDAndName{ - IDAndName: notify.IDAndName{ - ID: sr.Receiver, - Name: sr.RoleName, - }, - Scope: sr.RoleScope, - }) - case ReceiverDingtalkRobot, ReceiverFeishuRobot, ReceiverWorkwxRobot: - rows[i].Robot = sr.ReceiverType - case ReceiverWeebhook: - rows[i].Webhook = sr.ReceiverType - } - } - } - return rows -} - -type SSubscriptionReceiverDis struct { - SSubscriptionReceiver - ReceiverName string `json:"receiver_name"` - RoleName string `json:"role_name"` -} - -func (s *SSubscription) subscriptionReceiverDiss() ([]SSubscriptionReceiverDis, error) { - q := SubscriptionReceiverManager.Query().Equals("subscription_id", s.Id) - rq := ReceiverManager.Query("id", "name").SubQuery() - roq := db.RoleCacheManager.Query("id", "name").SubQuery() - q = q.LeftJoin(rq, sqlchemy.Equals(q.Field("receiver"), rq.Field("id"))) - q = q.LeftJoin(roq, sqlchemy.Equals(q.Field("receiver"), roq.Field("id"))) - // It looks strange, but the order of append cannot be changed - q.AppendField(q.QueryFields()...) - q.AppendField(rq.Field("name", "receiver_name")) - q.AppendField(roq.Field("name", "role_name")) - srs := make([]SSubscriptionReceiverDis, 0) - err := q.All(&srs) - if err != nil { - return nil, errors.Wrap(err, "unable to fetch All") - } - return srs, nil -} - -func (sm *SSubscriptionManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input jsonutils.JSONObject) (jsonutils.JSONObject, error) { - return nil, httperrors.NewForbiddenError("prohibit creation") -} - -func (ss *SSubscription) ValidateUpdateData(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input jsonutils.JSONObject) (jsonutils.JSONObject, error) { - return input, httperrors.NewForbiddenError("update prohibited") -} - -func (ss *SSubscription) ValidateDeleteCondition(ctx context.Context) error { - return httperrors.NewForbiddenError("prohibit deletion") -} - -func (ss *SSubscription) AllowPerformSetReceiver(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) bool { - return db.IsAdminAllowPerform(userCred, ss, "set-receiver") -} - -func (ss *SSubscription) PerformSetReceiver(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input notify.SubscriptionSetReceiverInput) (jsonutils.JSONObject, error) { - // check input - errgo, _ := errgroup.WithContext(ctx) - // check receiving roles - validScopes := []string{string(rbacutils.ScopeSystem), string(rbacutils.ScopeDomain), string(rbacutils.ScopeProject)} - for i := range input.ReceivingRoles { - role := input.ReceivingRoles[i] - index := i - errgo.Go(func() error { - if len(role.Scope) == 0 { - return httperrors.NewInputParameterError("empty scope for role %q", role.Role) - } - if !utils.IsInStringArray(role.Scope, validScopes) { - return httperrors.NewInputParameterError("invalid scope %q for role %q, need %s, %s or %s", role.Scope, role.Role, rbacutils.ScopeSystem, rbacutils.ScopeDomain, rbacutils.ScopeProject) - } - roleCache, err := db.RoleCacheManager.FetchRoleByIdOrName(ctx, role.Role) - if err != nil { - return errors.Wrapf(err, "unable find role %q", role) - } - input.ReceivingRoles[index].Role = roleCache.GetId() - return nil - }) - } - err := errgo.Wait() - if err != nil { - return nil, err - } - receivers, err := ReceiverManager.FetchByIdOrNames(ctx, input.Receivers...) - if err != nil { - return nil, errors.Wrap(err, "unable to fetch Receivers") - } - reSet := sets.NewString(input.Receivers...) - reIds := make([]string, len(receivers)) - for i := range receivers { - reSet.Delete(receivers[i].GetId()) - reSet.Delete(receivers[i].GetName()) - reIds[i] = receivers[i].GetId() - } - if reSet.Len() > 0 { - return nil, httperrors.NewInputParameterError("receivers %q not found", strings.Join(reSet.UnsortedList(), ", ")) - } - input.Receivers = reIds - - // deal with subscriptionReceivers - srs, err := SubscriptionReceiverManager.findReceivers(ss.Id, ReceiverNormal, ReceiverRole) - if err != nil { - return nil, errors.Wrap(err, "unable to findReceivers") - } - reSet = sets.NewString(input.Receivers...) - reRoleSet := make(map[notify.ReceivingRole]struct{}) - for _, role := range input.ReceivingRoles { - reRoleSet[role] = struct{}{} - } - for i := range srs { - rs := &srs[i] - switch rs.ReceiverType { - case ReceiverNormal: - if !reSet.Has(rs.Receiver) { - err := rs.Delete(ctx, userCred) - if err != nil { - return nil, errors.Wrapf(err, "unable to delete receiver %s", rs.Receiver) - } - } - reSet.Delete(rs.Receiver) - case ReceiverRole: - role := rs.receivingRole() - if _, ok := reRoleSet[role]; !ok { - err := rs.Delete(ctx, userCred) - if err != nil { - return nil, errors.Wrapf(err, "unable to delete receiver %s", rs.Receiver) - } - } - delete(reRoleSet, role) - } - } - for _, re := range reSet.UnsortedList() { - _, err := SubscriptionReceiverManager.create(ctx, ss.Id, re, ReceiverNormal, "") - if err != nil { - return nil, errors.Wrapf(err, "unable to create receiver %s", re) - } - } - for role := range reRoleSet { - _, err := SubscriptionReceiverManager.create(ctx, ss.Id, role.Role, ReceiverRole, role.Scope) - if err != nil { - return nil, errors.Wrapf(err, "unable to create role %s", role) - } - } - - return nil, nil -} - -func (ss *SSubscription) AllowPerformSetRobot(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) bool { - return db.IsAdminAllowPerform(userCred, ss, "set-robot") -} - -func (ss *SSubscription) PerformSetRobot(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input notify.SubscriptionSetRobotInput) (jsonutils.JSONObject, error) { - err := ss.setSingleReceiver(ctx, input.Robot, ReceiverDingtalkRobot, ReceiverFeishuRobot, ReceiverWorkwxRobot) - if errors.Cause(err) == errors.ErrNotFound { - return nil, httperrors.NewInputParameterError("unkown robot %q", input.Robot) - } - return nil, err -} - -func (ss *SSubscription) AllowPerformSetWebhook(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) bool { - return db.IsAdminAllowPerform(userCred, ss, "set-webhook") -} - -func (ss *SSubscription) PerformSetWebhook(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input notify.SubscriptionSetWebhookInput) (jsonutils.JSONObject, error) { - err := ss.setSingleReceiver(ctx, input.Webhook, ReceiverWeebhook) - if errors.Cause(err) == errors.ErrNotFound { - return nil, httperrors.NewInputParameterError("unkown webhook %q", input.Webhook) - } - return nil, err -} - -func (ss *SSubscription) setSingleReceiver(ctx context.Context, re string, reTypes ...string) error { - receiverRobot := reTypes - if !utils.IsInStringArray(re, receiverRobot) { - return errors.ErrNotFound - } - srs, err := SubscriptionReceiverManager.findReceivers(ss.Id, receiverRobot...) - if err != nil { - return errors.Wrap(err, "unable to findReceivers") - } - if len(srs) == 0 { - // create one - _, err := SubscriptionReceiverManager.create(ctx, ss.Id, "", re, "") - return err - } - if len(srs) > 1 { - return fmt.Errorf("multi robot receiver") - } - // update - sr := &srs[0] - _, err = db.Update(sr, func() error { - sr.ReceiverType = re - return nil - }) - return err -} - -func (s *SSubscription) addResources(resources ...string) { - for _, resource := range resources { - v := converter.resourceValue(resource) - if v < 0 { - continue - } - s.Resources += 1 << v - } -} - -func (s *SSubscription) addAction(actions ...notify.SAction) { - for _, action := range actions { - v := converter.actionValue(action) - if v < 0 { - continue - } - s.Actions += 1 << v - } -} - -func (s *SSubscription) getResources() []string { - vs := bitmap.Uint64ToIntArray(s.Resources) - resources := make([]string, 0, len(vs)) - for _, v := range vs { - resources = append(resources, converter.resource(v)) - } - return resources -} - -func (s *SSubscription) getActions() []notify.SAction { - vs := bitmap.Uint2IntArray(s.Actions) - actions := make([]notify.SAction, 0, len(vs)) - for _, v := range vs { - actions = append(actions, converter.action(v)) - } - return actions -} - -func (sm *SSubscriptionManager) SubsciptionByEvent(eventStr string, advanceDays int) ([]SSubscription, error) { - event, err := parseEvent(eventStr) - if err != nil { - return nil, errors.Wrapf(err, "unable to parse event %q", event) - } - resourceV := converter.resourceValue(event.ResourceType()) - actionV := converter.actionValue(event.Action()) - q := sm.Query().Equals("advance_days", advanceDays) - q = q.Filter(sqlchemy.GT(sqlchemy.AND_Val("", q.Field("resources"), 1<= len(rc.resourceList) { - return "" - } - return rc.resourceList[resourceValue] -} - -func (rc *sConverter) actionValue(action notify.SAction) int { - v, ok := rc.actionValueMap[action] - if !ok { - return -1 - } - return v -} - -func (rc *sConverter) action(actionValue int) notify.SAction { - if actionValue < 0 || actionValue >= len(rc.actionList) { - return notify.SAction("") - } - return rc.actionList[actionValue] -} diff --git a/pkg/notify/models/subscription_receiver.go b/pkg/notify/models/subscription_receiver.go deleted file mode 100644 index 1fd1ef071c..0000000000 --- a/pkg/notify/models/subscription_receiver.go +++ /dev/null @@ -1,201 +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 models - -import ( - "context" - "fmt" - - "golang.org/x/sync/errgroup" - - "yunion.io/x/jsonutils" - "yunion.io/x/log" - "yunion.io/x/pkg/errors" - - "yunion.io/x/onecloud/pkg/apis/notify" - "yunion.io/x/onecloud/pkg/cloudcommon/db" - "yunion.io/x/onecloud/pkg/mcclient/auth" - "yunion.io/x/onecloud/pkg/mcclient/modules" -) - -var SubscriptionReceiverManager *SSubscriptionReceiverManager - -func init() { - SubscriptionReceiverManager = &SSubscriptionReceiverManager{ - SStandaloneResourceBaseManager: db.NewStandaloneResourceBaseManager( - SSubscriptionReceiver{}, - "subscriptionreceiver_tbl", - "subscriptionreceiver", - "subscriptionreceivers", - ), - } - SubscriptionReceiverManager.SetVirtualObject(ReceiverNotificationManager) -} - -type SSubscriptionReceiverManager struct { - db.SStandaloneResourceBaseManager -} - -type SSubscriptionReceiver struct { - db.SStandaloneResourceBase - - SubscriptionID string `width:"128" charset:"ascii" nullable:"fase" index:"true"` - // role id or receiver id or other and the value type is determined by the ReceiverType - Receiver string `width:"128" charset:"ascii" nullable:"false"` - ReceiverType string `width:"16" charset:"ascii" nullable:"false" index:"true"` - RoleScope string `width:"8" charset:"ascii" nullable:"false"` -} - -const ( - ReceiverRole = "role" - ReceiverNormal = "normal" - ReceiverFeishuRobot = notify.FEISHU_ROBOT - ReceiverDingtalkRobot = notify.DINGTALK_ROBOT - ReceiverWorkwxRobot = notify.WORKWX_ROBOT - ReceiverWeebhook = "webhook" - - ScopeSystem = "system" - ScopeDomain = "domain" - ScopeProject = "project" -) - -func (srm *SSubscriptionReceiverManager) robot(ssid string) (string, error) { - return srm.findSingleReceiver(ReceiverFeishuRobot, ReceiverDingtalkRobot, ReceiverWorkwxRobot) -} - -func (srm *SSubscriptionReceiverManager) webhook(ssid string) (string, error) { - return srm.findSingleReceiver(ssid, ReceiverWeebhook) -} - -func (srm *SSubscriptionReceiverManager) findSingleReceiver(ssid string, receiverTypes ...string) (string, error) { - srs, err := srm.findReceivers(ssid, receiverTypes...) - if err != nil { - return "", err - } - if len(srs) > 1 { - return "", errors.Error("multi receiver") - } - if len(srs) == 0 { - return "", errors.ErrNotFound - } - return srs[0].ReceiverType, nil -} - -func (srm *SSubscriptionReceiverManager) findReceivers(ssid string, receiverTypes ...string) ([]SSubscriptionReceiver, error) { - if len(receiverTypes) == 0 { - return nil, nil - } - q := srm.Query().Equals("subscription_id", ssid) - if len(receiverTypes) == 1 { - q = q.Equals("receiver_type", receiverTypes[0]) - } else { - q = q.In("receiver_type", receiverTypes) - } - srs := make([]SSubscriptionReceiver, 0) - err := db.FetchModelObjects(srm, q, &srs) - if err != nil { - return nil, errors.Wrap(err, "unable to FetchModelObjects") - } - return srs, nil -} - -// TODO: Use cache to increase speed -func (srm *SSubscriptionReceiverManager) getReceivers(ctx context.Context, ssid string, projectDomainId string, projectId string) ([]string, error) { - srs, err := srm.findReceivers(ssid, ReceiverRole, ReceiverNormal) - if err != nil { - return nil, err - } - receivers := make([]string, 0, len(srs)) - roleMap := make(map[string][]string, 3) - receivermap := make(map[string]*[]string, 3) - for _, sr := range srs { - if sr.ReceiverType == ReceiverNormal { - receivers = append(receivers, sr.Receiver) - } else if sr.ReceiverType == ReceiverRole { - roleMap[sr.RoleScope] = append(roleMap[sr.RoleScope], sr.Receiver) - receivermap[sr.RoleScope] = &[]string{} - } - } - errgo, _ := errgroup.WithContext(ctx) - for scope, roles := range roleMap { - receivers := receivermap[scope] - errgo.Go(func() error { - query := jsonutils.NewDict() - query.Set("roles", jsonutils.NewStringArray(roles)) - query.Set("effective", jsonutils.JSONTrue) - switch scope { - case ScopeSystem: - case ScopeDomain: - if len(projectDomainId) == 0 { - return fmt.Errorf("need projectDomainId") - } - query.Set("project_domain_id", jsonutils.NewString(projectDomainId)) - case ScopeProject: - if len(projectId) == 0 { - return fmt.Errorf("need projectId") - } - query.Add(jsonutils.NewString(projectId), "scope", "project", "id") - } - s := auth.GetAdminSession(ctx, "", "") - log.Debugf("query for role-assignments: %s", query.String()) - listRet, err := modules.RoleAssignments.List(s, query) - if err != nil { - return errors.Wrap(err, "unable to list RoleAssignments") - } - log.Debugf("return value for role-assignments: %s", jsonutils.Marshal(listRet)) - for i := range listRet.Data { - ras := listRet.Data[i] - user, err := ras.Get("user") - if err == nil { - id, err := user.GetString("id") - if err != nil { - return errors.Wrap(err, "unable to get user.id from result of RoleAssignments.List") - } - *receivers = append(*receivers, id) - } - } - return nil - }) - } - err = errgo.Wait() - if err != nil { - return nil, err - } - for _, res := range receivermap { - receivers = append(receivers, *res...) - } - return receivers, nil -} - -func (srm *SSubscriptionReceiverManager) create(ctx context.Context, ssid, receiver, receiverType, roleScope string) (*SSubscriptionReceiver, error) { - sr := &SSubscriptionReceiver{ - SubscriptionID: ssid, - Receiver: receiver, - ReceiverType: receiverType, - RoleScope: roleScope, - } - err := srm.TableSpec().Insert(ctx, sr) - if err != nil { - return nil, errors.Wrap(err, "unable to insert") - } - return sr, nil -} - -func (sr *SSubscriptionReceiver) receivingRole() notify.ReceivingRole { - return notify.ReceivingRole{ - Role: sr.Receiver, - Scope: sr.RoleScope, - } -} diff --git a/pkg/notify/models/topic.go b/pkg/notify/models/topic.go new file mode 100644 index 0000000000..5610e86efa --- /dev/null +++ b/pkg/notify/models/topic.go @@ -0,0 +1,403 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package models + +import ( + "context" + "fmt" + "strings" + + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/tristate" + "yunion.io/x/pkg/util/sets" + "yunion.io/x/sqlchemy" + + "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" + "yunion.io/x/onecloud/pkg/util/bitmap" + "yunion.io/x/onecloud/pkg/util/stringutils2" +) + +func parseEvent(es string) (notify.SEvent, error) { + ess := strings.Split(es, notify.DelimiterInEvent) + if len(ess) != 2 { + return notify.SEvent{}, fmt.Errorf("invalid event string %q", es) + } + return notify.Event.WithResourceType(ess[0]).WithAction(notify.SAction(ess[1])), nil +} + +type STopicManager struct { + db.SStandaloneResourceBaseManager + db.SEnabledResourceBaseManager +} + +var TopicManager *STopicManager + +func init() { + TopicManager = &STopicManager{ + SStandaloneResourceBaseManager: db.NewStandaloneResourceBaseManager( + STopic{}, + "topic_tbl", + "topic", + "topics", + ), + } + TopicManager.SetVirtualObject(TopicManager) +} + +type STopic struct { + db.SStandaloneResourceBase + db.SEnabledResourceBase + + Type string `width:"20" nullable:"false" create:"required" update:"user" list:"user"` + Resources uint64 `nullable:"false"` + Actions uint32 `nullable:"false"` + AdvanceDays int `nullable:"false"` +} + +const ( + DefaultResourceCreateDelete = "resource create or delete" + DefaultResourceChangeConfig = "resource change config" + DefaultResourceUpdate = "resource update" + DefaultResourceReleaseDue1Day = "resource release due 1 day" + DefaultResourceReleaseDue3Day = "resource release due 3 day" + DefaultScheduledTaskExecute = "scheduled task execute" + DefaultScalingPolicyExecute = "scaling policy execute" + DefaultSnapshotPolicyExecute = "snapshot policy execute" +) + +func (sm *STopicManager) InitializeData() error { + initSNames := sets.NewString( + DefaultResourceCreateDelete, + DefaultResourceChangeConfig, + DefaultResourceUpdate, + DefaultResourceReleaseDue1Day, + DefaultResourceReleaseDue3Day, + DefaultScheduledTaskExecute, + DefaultScalingPolicyExecute, + DefaultSnapshotPolicyExecute, + ) + q := sm.Query() + topics := make([]STopic, 0, initSNames.Len()) + err := db.FetchModelObjects(sm, q, &topics) + if err != nil { + return errors.Wrap(err, "unable to FetchModelObjects") + } + for i := range topics { + t := &topics[i] + initSNames.Delete(t.Name) + } + ctx := context.Background() + for _, name := range initSNames.UnsortedList() { + t := new(STopic) + t.Name = name + t.Enabled = tristate.True + switch name { + case DefaultResourceCreateDelete: + t.addResources( + notify.TOPIC_RESOURCE_SERVER, + notify.TOPIC_RESOURCE_SCALINGGROUP, + notify.TOPIC_RESOURCE_IMAGE, + notify.TOPIC_RESOURCE_DISK, + notify.TOPIC_RESOURCE_SNAPSHOT, + notify.TOPIC_RESOURCE_INSTANCESNAPSHOT, + notify.TOPIC_RESOURCE_SNAPSHOTPOLICY, + notify.TOPIC_RESOURCE_NETWORK, + notify.TOPIC_RESOURCE_EIP, + notify.TOPIC_RESOURCE_LOADBALANCER, + notify.TOPIC_RESOURCE_LOADBALANCERACL, + notify.TOPIC_RESOURCE_LOADBALANCERCERTIFICATE, + notify.TOPIC_RESOURCE_BUCKET, + notify.TOPIC_RESOURCE_DBINSTANCE, + notify.TOPIC_RESOURCE_ELASTICCACHE, + ) + t.addAction( + notify.ActionCreate, + notify.ActionDelete, + notify.ActionPendingDelete, + ) + t.Type = notify.TOPIC_TYPE_RESOURCE + case DefaultResourceChangeConfig: + t.addResources( + notify.TOPIC_RESOURCE_SERVER, + notify.TOPIC_RESOURCE_DISK, + notify.TOPIC_RESOURCE_DBINSTANCE, + notify.TOPIC_RESOURCE_ELASTICCACHE, + ) + t.addAction(notify.ActionChangeConfig) + t.Type = notify.TOPIC_TYPE_RESOURCE + case DefaultResourceUpdate: + t.addResources( + notify.TOPIC_RESOURCE_SERVER, + notify.TOPIC_RESOURCE_DISK, + notify.TOPIC_RESOURCE_DBINSTANCE, + notify.TOPIC_RESOURCE_ELASTICCACHE, + ) + t.addAction(notify.ActionUpdate) + t.Type = notify.TOPIC_TYPE_RESOURCE + case DefaultResourceReleaseDue1Day: + t.addResources( + notify.TOPIC_RESOURCE_SERVER, + notify.TOPIC_RESOURCE_DISK, + notify.TOPIC_RESOURCE_EIP, + notify.TOPIC_RESOURCE_LOADBALANCER, + notify.TOPIC_RESOURCE_DBINSTANCE, + notify.TOPIC_RESOURCE_ELASTICCACHE, + ) + t.addAction(notify.ActionExpiredRelease) + t.Type = notify.TOPIC_TYPE_RESOURCE + t.AdvanceDays = 1 + case DefaultResourceReleaseDue3Day: + t.addResources( + notify.TOPIC_RESOURCE_SERVER, + notify.TOPIC_RESOURCE_DISK, + notify.TOPIC_RESOURCE_EIP, + notify.TOPIC_RESOURCE_LOADBALANCER, + notify.TOPIC_RESOURCE_DBINSTANCE, + notify.TOPIC_RESOURCE_ELASTICCACHE, + ) + t.addAction(notify.ActionExpiredRelease) + t.Type = notify.TOPIC_TYPE_RESOURCE + t.AdvanceDays = 3 + case DefaultScheduledTaskExecute: + t.addResources(notify.TOPIC_RESOURCE_SCHEDULEDTASK) + t.addAction(notify.ActionExecute) + t.Type = notify.TOPIC_TYPE_AUTOMATED_PROCESS + case DefaultScalingPolicyExecute: + t.addResources(notify.TOPIC_RESOURCE_SCALINGPOLICY) + t.addAction(notify.ActionExecute) + t.Type = notify.TOPIC_TYPE_AUTOMATED_PROCESS + case DefaultSnapshotPolicyExecute: + t.addResources(notify.TOPIC_RESOURCE_SNAPSHOTPOLICY) + t.addAction(notify.ActionExecute) + t.Type = notify.TOPIC_TYPE_AUTOMATED_PROCESS + } + err := sm.TableSpec().Insert(ctx, t) + if err != nil { + return errors.Wrapf(err, "unable to insert %s", name) + } + } + return nil +} + +func (sm *STopicManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, input notify.TopicListInput) (*sqlchemy.SQuery, error) { + return sm.SStandaloneResourceBaseManager.ListItemFilter(ctx, q, userCred, input.StandaloneResourceListInput) +} + +func (sm *STopicManager) FetchCustomizeColumns(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, objs []interface{}, fields stringutils2.SSortedStrings, isList bool) []notify.TopicDetails { + sRows := sm.SStandaloneResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList) + rows := make([]notify.TopicDetails, len(objs)) + for i := range rows { + rows[i].StandaloneResourceDetails = sRows[i] + ss := objs[i].(*STopic) + rows[i].Resources = ss.getResources() + } + return rows +} + +type SSubscriberDis struct { + SSubscriber + ReceiverName string `json:"receiver_name"` + RoleName string `json:"role_name"` +} + +func (s *STopic) subscriptionReceiverDiss() ([]SSubscriberDis, error) { + q := SubscriberManager.Query().Equals("subscription_id", s.Id) + rq := ReceiverManager.Query("id", "name").SubQuery() + roq := db.RoleCacheManager.Query("id", "name").SubQuery() + q = q.LeftJoin(rq, sqlchemy.Equals(q.Field("receiver"), rq.Field("id"))) + q = q.LeftJoin(roq, sqlchemy.Equals(q.Field("receiver"), roq.Field("id"))) + // It looks strange, but the order of append cannot be changed + q.AppendField(q.QueryFields()...) + q.AppendField(rq.Field("name", "receiver_name")) + q.AppendField(roq.Field("name", "role_name")) + srs := make([]SSubscriberDis, 0) + err := q.All(&srs) + if err != nil { + return nil, errors.Wrap(err, "unable to fetch All") + } + return srs, nil +} + +func (sm *STopicManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input jsonutils.JSONObject) (jsonutils.JSONObject, error) { + return nil, httperrors.NewForbiddenError("prohibit creation") +} + +func (ss *STopic) ValidateUpdateData(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input jsonutils.JSONObject) (jsonutils.JSONObject, error) { + return input, httperrors.NewForbiddenError("update prohibited") +} + +func (ss *STopic) ValidateDeleteCondition(ctx context.Context) error { + return httperrors.NewForbiddenError("prohibit deletion") +} + +func (s *STopic) addResources(resources ...string) { + for _, resource := range resources { + v := converter.resourceValue(resource) + if v < 0 { + continue + } + s.Resources += 1 << v + } +} + +func (s *STopic) addAction(actions ...notify.SAction) { + for _, action := range actions { + v := converter.actionValue(action) + if v < 0 { + continue + } + s.Actions += 1 << v + } +} + +func (s *STopic) getResources() []string { + vs := bitmap.Uint64ToIntArray(s.Resources) + resources := make([]string, 0, len(vs)) + for _, v := range vs { + resources = append(resources, converter.resource(v)) + } + return resources +} + +func (s *STopic) getActions() []notify.SAction { + vs := bitmap.Uint2IntArray(s.Actions) + actions := make([]notify.SAction, 0, len(vs)) + for _, v := range vs { + actions = append(actions, converter.action(v)) + } + return actions +} + +func (sm *STopicManager) TopicsByEvent(eventStr string, advanceDays int) ([]STopic, error) { + event, err := parseEvent(eventStr) + if err != nil { + return nil, errors.Wrapf(err, "unable to parse event %q", event) + } + resourceV := converter.resourceValue(event.ResourceType()) + actionV := converter.actionValue(event.Action()) + q := sm.Query().Equals("advance_days", advanceDays) + q = q.Filter(sqlchemy.GT(sqlchemy.AND_Val("", q.Field("resources"), 1<= len(rc.resourceList) { + return "" + } + return rc.resourceList[resourceValue] +} + +func (rc *sConverter) actionValue(action notify.SAction) int { + v, ok := rc.actionValueMap[action] + if !ok { + return -1 + } + return v +} + +func (rc *sConverter) action(actionValue int) notify.SAction { + if actionValue < 0 || actionValue >= len(rc.actionList) { + return notify.SAction("") + } + return rc.actionList[actionValue] +} diff --git a/pkg/notify/service/handlers.go b/pkg/notify/service/handlers.go index 89d012ed45..048b0e4cb0 100644 --- a/pkg/notify/service/handlers.go +++ b/pkg/notify/service/handlers.go @@ -39,7 +39,6 @@ func InitHandlers(app *appsrv.Application) { models.SubContactManager, db.SharedResourceManager, models.VerificationManager, - models.SubscriptionReceiverManager, } { db.RegisterModelManager(manager) } @@ -51,8 +50,9 @@ func InitHandlers(app *appsrv.Application) { models.NotificationManager, models.ConfigManager, models.TemplateManager, - models.SubscriptionManager, + models.TopicManager, models.RobotManager, + models.SubscriberManager, } { db.RegisterModelManager(manager) handler := db.NewModelHandler(manager)