// 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", "成功", }, ) }