mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-10-24 23:41:52 +08:00
582 lines
14 KiB
Go
582 lines
14 KiB
Go
// 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"
|
|
"html"
|
|
"html/template"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/Masterminds/sprig"
|
|
"golang.org/x/text/language"
|
|
|
|
"yunion.io/x/jsonutils"
|
|
"yunion.io/x/pkg/errors"
|
|
|
|
comapi "yunion.io/x/onecloud/pkg/apis/compute"
|
|
"yunion.io/x/onecloud/pkg/apis/notify"
|
|
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 {
|
|
sEvenWebhookMsg
|
|
ResourceTypeDisplay string
|
|
ActionDisplay string
|
|
AdvanceDays int
|
|
}
|
|
|
|
type sEvenWebhookMsg struct {
|
|
ResourceType string `json:"resource_type"`
|
|
Action string `json:"action"`
|
|
ResourceDetails map[string]interface{} `json:"resource_details"`
|
|
}
|
|
|
|
// templateDir = "/opt/yunion/share/local-templates"
|
|
type SLocalTemplateManager struct {
|
|
templateDir string
|
|
templatesTable *sync.Map
|
|
}
|
|
|
|
var LocalTemplateManager *SLocalTemplateManager
|
|
|
|
func init() {
|
|
LocalTemplateManager = &SLocalTemplateManager{
|
|
templateDir: "/opt/yunion/share/local-templates",
|
|
templatesTable: &sync.Map{},
|
|
}
|
|
}
|
|
|
|
func languageTag(lang string) language.Tag {
|
|
var langStr string
|
|
if lang == api.TEMPLATE_LANG_CN {
|
|
langStr = "zh-CN"
|
|
} else {
|
|
langStr = "en"
|
|
}
|
|
t, _ := language.Parse(langStr)
|
|
return t
|
|
}
|
|
|
|
func (lt *SLocalTemplateManager) detailsDisplay(resourceType string, details *jsonutils.JSONDict, tag language.Tag) {
|
|
fields, ok := specFields[resourceType]
|
|
if !ok {
|
|
return
|
|
}
|
|
for _, field := range fields {
|
|
if !details.Contains(field) {
|
|
continue
|
|
}
|
|
v, _ := details.GetString(field)
|
|
dv := specFieldTrans[resourceType].LookupByLang(tag, v)
|
|
details.Set(field+"_display", jsonutils.NewString(dv))
|
|
}
|
|
}
|
|
|
|
func (lt *SLocalTemplateManager) FillWithTemplate(ctx context.Context, lang string, no notifyv2.SNotification) (params rpcapi.SendParams, err error) {
|
|
out, event := rpcapi.SendParams{}, no.Event
|
|
rtStr, aStr, resultStr := event.ResourceType(), string(event.Action()), string(event.Result())
|
|
msgObj, err := jsonutils.ParseString(no.Message)
|
|
if err != nil {
|
|
return out, errors.Wrapf(err, "unable to parse json from %q", no.Message)
|
|
}
|
|
msg := msgObj.(*jsonutils.JSONDict)
|
|
if info, _ := TemplateManager.GetCompanyInfo(ctx); len(info.Name) > 0 {
|
|
msg.Set("brand", jsonutils.NewString(info.Name))
|
|
}
|
|
webhookMsg := jsonutils.NewDict()
|
|
webhookMsg.Set("resource_type", jsonutils.NewString(rtStr))
|
|
webhookMsg.Set("action", jsonutils.NewString(aStr))
|
|
webhookMsg.Set("result", jsonutils.NewString(resultStr))
|
|
webhookMsg.Set("resource_details", msg)
|
|
if no.ContactType == api.WEBHOOK {
|
|
return rpcapi.SendParams{
|
|
Title: no.Event.StringWithDeli("_"),
|
|
Message: webhookMsg.String(),
|
|
}, nil
|
|
}
|
|
|
|
if lang == "" {
|
|
lang = getLangSuffix(ctx)
|
|
}
|
|
|
|
tag := languageTag(lang)
|
|
rtDis := notifyclientI18nTable.LookupByLang(tag, rtStr)
|
|
if len(rtDis) == 0 {
|
|
rtDis = rtStr
|
|
}
|
|
aDis := notifyclientI18nTable.LookupByLang(tag, aStr)
|
|
if len(aDis) == 0 {
|
|
aDis = aStr
|
|
}
|
|
resultDis := notifyclientI18nTable.LookupByLang(tag, resultStr)
|
|
if len(resultDis) == 0 {
|
|
resultDis = resultStr
|
|
}
|
|
|
|
lt.detailsDisplay(rtStr, msg, tag)
|
|
|
|
templateParams := webhookMsg
|
|
templateParams.Set("advance_days", jsonutils.NewInt(int64(no.AdvanceDays)))
|
|
templateParams.Set("resource_type_display", jsonutils.NewString(rtDis))
|
|
templateParams.Set("action_display", jsonutils.NewString(aDis))
|
|
templateParams.Set("result_display", jsonutils.NewString(resultDis))
|
|
|
|
// get title
|
|
title, err := lt.fillWithTemplate(ctx, "title", no.ContactType, lang, event, templateParams)
|
|
if err != nil {
|
|
if errors.Cause(err) == errors.ErrNotFound {
|
|
title = no.Topic
|
|
} else {
|
|
return out, err
|
|
}
|
|
}
|
|
|
|
// get content
|
|
content, err := lt.fillWithTemplate(ctx, "content", no.ContactType, lang, event, templateParams)
|
|
if err != nil {
|
|
if errors.Cause(err) == errors.ErrNotFound {
|
|
content = no.Message
|
|
} else {
|
|
return out, err
|
|
}
|
|
}
|
|
|
|
out.Title = html.UnescapeString(title)
|
|
out.Message = html.UnescapeString(content)
|
|
return out, nil
|
|
}
|
|
|
|
var action2Topic = make(map[string]string, 0)
|
|
|
|
func specTopic(event api.SNotifyEvent) string {
|
|
switch event.Action() {
|
|
case api.ActionRebuildRoot, api.ActionChangeIpaddr, api.ActionResetPassword:
|
|
return string(api.ActionUpdate)
|
|
case api.ActionDelete:
|
|
switch event.ResourceType() {
|
|
case api.TOPIC_RESOURCE_BAREMETAL, api.TOPIC_RESOURCE_SERVER, api.TOPIC_RESOURCE_LOADBALANCER, api.TOPIC_RESOURCE_DBINSTANCE, api.TOPIC_RESOURCE_ELASTICCACHE:
|
|
return "DELETE_WITH_IP"
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func init() {
|
|
action2Topic[string(api.ActionRebuildRoot)] = string(api.ActionUpdate)
|
|
action2Topic[string(api.ActionResetPassword)] = string(api.ActionUpdate)
|
|
action2Topic[string(api.ActionChangeIpaddr)] = string(api.ActionUpdate)
|
|
}
|
|
|
|
func (lt *SLocalTemplateManager) fillWithTemplate(ctx context.Context, titleOrContent string, contactType string, lang string, event api.SNotifyEvent, dis jsonutils.JSONObject) (string, error) {
|
|
var (
|
|
tmpl *template.Template
|
|
err error
|
|
)
|
|
actionResultStr := event.ActionWithResult("_")
|
|
for _, topic := range []string{specTopic(event), event.StringWithDeli("_"), actionResultStr, "common"} {
|
|
if topic == "" {
|
|
continue
|
|
}
|
|
tmpl, err = lt.getTemplate(ctx, titleOrContent, contactType, topic, lang)
|
|
if errors.Cause(err) == errors.ErrNotFound {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "unable to getTemplate")
|
|
}
|
|
break
|
|
}
|
|
if tmpl == nil {
|
|
return "", errors.ErrNotFound
|
|
}
|
|
|
|
buf := strings.Builder{}
|
|
err = tmpl.Execute(&buf, dis.Interface())
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "template.Execute")
|
|
}
|
|
return buf.String(), nil
|
|
}
|
|
|
|
var specFields = map[string][]string{
|
|
notify.TOPIC_RESOURCE_SCALINGPOLICY: {
|
|
"trigger_type",
|
|
"action",
|
|
"unit",
|
|
},
|
|
notify.TOPIC_RESOURCE_SCHEDULEDTASK: {
|
|
"resource_type",
|
|
"operation",
|
|
},
|
|
}
|
|
|
|
var specFieldTrans = map[string]i18n.Table{}
|
|
|
|
func init() {
|
|
var spI18nTable = i18n.Table{}
|
|
spI18nTable.Set(comapi.TRIGGER_ALARM, i18n.NewTableEntry().EN("alarm").CN("告警"))
|
|
spI18nTable.Set(comapi.TRIGGER_TIMING, i18n.NewTableEntry().EN("timing").CN("定时"))
|
|
spI18nTable.Set(comapi.TRIGGER_CYCLE, i18n.NewTableEntry().EN("cycle").CN("周期"))
|
|
spI18nTable.Set(comapi.ACTION_ADD, i18n.NewTableEntry().EN("add").CN("增加"))
|
|
spI18nTable.Set(comapi.ACTION_REMOVE, i18n.NewTableEntry().EN("remove").CN("减少"))
|
|
spI18nTable.Set(comapi.ACTION_SET, i18n.NewTableEntry().EN("set as").CN("设置为"))
|
|
spI18nTable.Set(comapi.UNIT_ONE, i18n.NewTableEntry().EN("").CN("个"))
|
|
spI18nTable.Set(comapi.UNIT_PERCENT, i18n.NewTableEntry().EN("%").CN("%"))
|
|
|
|
var stI18nTable = i18n.Table{}
|
|
stI18nTable.Set(schapi.ST_RESOURCE_SERVER, i18n.NewTableEntry().EN("virtual machine").CN("虚拟机"))
|
|
stI18nTable.Set(schapi.ST_RESOURCE_CLOUDACCOUNT, i18n.NewTableEntry().EN("cloud account").CN("云账号"))
|
|
stI18nTable.Set(schapi.ST_RESOURCE_OPERATION_RESTART, i18n.NewTableEntry().EN("restart").CN("重启"))
|
|
stI18nTable.Set(schapi.ST_RESOURCE_OPERATION_STOP, i18n.NewTableEntry().EN("stop").CN("关机"))
|
|
stI18nTable.Set(schapi.ST_RESOURCE_OPERATION_START, i18n.NewTableEntry().EN("start").CN("开机"))
|
|
stI18nTable.Set(schapi.ST_RESOURCE_OPERATION_SYNC, i18n.NewTableEntry().EN("sync").CN("同步"))
|
|
|
|
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) {
|
|
key := fmt.Sprintf("%s.%s@%s", topic, titleOrContent, lang)
|
|
|
|
obj, ok := lt.templatesTable.Load(key)
|
|
var elem sTemplateElem
|
|
if !ok {
|
|
// read from file
|
|
cont, err := lt.getTemplateString(ctx, titleOrContent, "", topic, lang)
|
|
if err != nil {
|
|
if err == errors.ErrNotFound {
|
|
elem = sTemplateElem{
|
|
template: nil,
|
|
}
|
|
}
|
|
return nil, err
|
|
} else {
|
|
tmp := template.New(key)
|
|
tmp.Funcs(sprig.FuncMap())
|
|
tmp, err = tmp.Parse(string(cont))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
elem = sTemplateElem{
|
|
template: tmp,
|
|
}
|
|
}
|
|
lt.templatesTable.Store(key, elem)
|
|
} else {
|
|
elem = obj.(sTemplateElem)
|
|
}
|
|
if elem.template == nil {
|
|
return nil, errors.ErrNotFound
|
|
}
|
|
return elem.template, nil
|
|
}
|
|
|
|
func (lt *SLocalTemplateManager) getTemplateString(ctx context.Context, titleOrContent string, contactType string, topic string, lang string) ([]byte, error) {
|
|
topic = strings.ToUpper(topic)
|
|
titleOrContent = titleOrContent + "@" + lang
|
|
var path string
|
|
if len(contactType) > 0 {
|
|
path = filepath.Join(lt.templateDir, titleOrContent, contactType, fmt.Sprintf("%s.tmpl", topic))
|
|
} else {
|
|
path = filepath.Join(lt.templateDir, titleOrContent, fmt.Sprintf("%s.tmpl", topic))
|
|
}
|
|
content, err := ioutil.ReadFile(path)
|
|
if err != nil {
|
|
if _, ok := err.(*os.PathError); ok {
|
|
return nil, errors.ErrNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
return content, nil
|
|
}
|
|
|
|
var (
|
|
notifyclientI18nTable = i18n.Table{}
|
|
)
|
|
|
|
type sTemplateElem struct {
|
|
template *template.Template
|
|
}
|
|
|
|
func setI18nTable(t i18n.Table, elems ...sI18nElme) {
|
|
for i := range elems {
|
|
t.Set(elems[i].k, i18n.NewTableEntry().EN(elems[i].en).CN(elems[i].cn))
|
|
}
|
|
}
|
|
|
|
func getLangSuffix(ctx context.Context) string {
|
|
return notifyclientI18nTable.Lookup(ctx, tempalteLang)
|
|
}
|
|
|
|
const (
|
|
tempalteLang = "lang"
|
|
)
|
|
|
|
type sI18nElme struct {
|
|
k string
|
|
en string
|
|
cn string
|
|
}
|
|
|
|
func init() {
|
|
setI18nTable(notifyclientI18nTable,
|
|
sI18nElme{
|
|
tempalteLang,
|
|
api.TEMPLATE_LANG_EN,
|
|
api.TEMPLATE_LANG_CN,
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_SERVER,
|
|
"virtual machine",
|
|
"虚拟机",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_SCALINGGROUP,
|
|
"scaling group",
|
|
"弹性伸缩组",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_SCALINGPOLICY,
|
|
"scaling policy",
|
|
"弹性伸缩策略",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_IMAGE,
|
|
"image",
|
|
"系统镜像",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_DISK,
|
|
"disk",
|
|
"硬盘",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_SNAPSHOT,
|
|
"snapshot",
|
|
"硬盘快照",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_INSTANCESNAPSHOT,
|
|
"instance snapshot",
|
|
"主机快照",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_NETWORK,
|
|
"network",
|
|
"IP子网",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_EIP,
|
|
"EIP",
|
|
"弹性公网IP",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_SECGROUP,
|
|
"security group",
|
|
"安全组",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_LOADBALANCER,
|
|
"loadbalancer instance",
|
|
"负载均衡实例",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_LOADBALANCERACL,
|
|
"loadbalancer ACL",
|
|
"负载均衡访问控制",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_LOADBALANCERCERTIFICATE,
|
|
"loadbalancer certificate",
|
|
"负载均衡证书",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_LOADBALANCERLISTENER,
|
|
"loadbalancer listener",
|
|
"负载均衡监听",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_LOADBALANCERBACKEDNGROUP,
|
|
"loadbalancer backendgroup",
|
|
"负载均衡服务器组",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_BUCKET,
|
|
"object storage bucket",
|
|
"对象存储桶",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_DBINSTANCE,
|
|
"RDS instance",
|
|
"RDS实例",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_ELASTICCACHE,
|
|
"Redis instance",
|
|
"Redis实例",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_SCHEDULEDTASK,
|
|
"scheduled task",
|
|
"定时任务",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_BAREMETAL,
|
|
"baremetal",
|
|
"裸金属",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_SNAPSHOTPOLICY,
|
|
"snapshot policy",
|
|
"快照策略",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_VPC,
|
|
"VPC",
|
|
"VPC",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_DNSZONE,
|
|
"DNS zone",
|
|
"DNS zone",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_DNSRECORDSET,
|
|
"DNS record",
|
|
"DNS 记录",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_NATGATEWAY,
|
|
"nat gateway",
|
|
"nat网关",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_WEBAPP,
|
|
"webapp",
|
|
"应用程序服务",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_CDNDOMAIN,
|
|
"CDN domain",
|
|
"CDN domain",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_FILESYSTEM,
|
|
"file system",
|
|
"文件系统",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_WAF,
|
|
"WAF",
|
|
"WAF",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_KAFKA,
|
|
"Kafka",
|
|
"Kafka",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_ELASTICSEARCH,
|
|
"Elasticsearch",
|
|
"Elasticsearch",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_MONGODB,
|
|
"MongoDB",
|
|
"MongoDB",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_DB_TABLE_RECORD,
|
|
"database table record",
|
|
"数据库记录",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_ACTION_LOG,
|
|
"action log",
|
|
"操作日志",
|
|
},
|
|
sI18nElme{
|
|
api.TOPIC_RESOURCE_CLOUDPODS_COMPONENT,
|
|
"cloudpods component",
|
|
"cloudpods服务组件",
|
|
},
|
|
sI18nElme{
|
|
string(api.ActionCreate),
|
|
"created",
|
|
"创建",
|
|
},
|
|
sI18nElme{
|
|
string(api.ActionDelete),
|
|
"deleted",
|
|
"删除",
|
|
},
|
|
sI18nElme{
|
|
string(api.ActionRebuildRoot),
|
|
"rebuilded root",
|
|
"重装系统",
|
|
},
|
|
sI18nElme{
|
|
string(api.ActionResetPassword),
|
|
"reseted password",
|
|
"重置密码",
|
|
},
|
|
sI18nElme{
|
|
string(api.ActionChangeConfig),
|
|
"changed config",
|
|
"更改配置",
|
|
},
|
|
sI18nElme{
|
|
string(api.ActionResize),
|
|
"resize",
|
|
"扩容",
|
|
},
|
|
sI18nElme{
|
|
string(api.ActionExpiredRelease),
|
|
"expired and released",
|
|
"到期释放",
|
|
},
|
|
sI18nElme{
|
|
string(api.ActionExecute),
|
|
"executed",
|
|
"生效执行",
|
|
},
|
|
sI18nElme{
|
|
string(api.ActionPendingDelete),
|
|
"added to the recycle bin",
|
|
"加入回收站",
|
|
},
|
|
sI18nElme{
|
|
string(api.ResultFailed),
|
|
"failed",
|
|
"失败",
|
|
},
|
|
sI18nElme{
|
|
string(api.ResultSucceed),
|
|
"successfully",
|
|
"成功",
|
|
},
|
|
)
|
|
}
|