diff --git a/.gitignore b/.gitignore index 8c2b884..c3c377d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,38 @@ # Local History for Visual Studio Code .history/ +# Run temp file and dir +crontask/log/ +crontask/ftp/ +crontask/database/ +crontask/export/ +crontask/temp +crontask/crontask + +restagent/backup/ +restagent/log/ +restagent/upload/ +restagent/software/ +restagent/database/ +restagent/restagent + +sshsvc/sshsvc +sshsvc/mmllog/ +sshsvc/mmlhome/ +sshsvc/log/ + +captrace/captrace +captrace/log/ + +tools/loadmconf/loadmconf +tools/loadpconf/loadpconf + +vendor + # Built Visual Studio Code Extensions *.vsix +*.log +*.log-* +*.bak +__debug_bin*.exe diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..7e257db --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": [] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..8f7c445 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,41 @@ +{ + // 使用 IntelliSense 了解相关属性。 + // 悬停以查看现有属性的描述。 + // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "调试模式", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "./restagent/restagent.go", + "console": "integratedTerminal" + }, + { + "name": "DEBUG restagent", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "d:/local.git/ems.agt/restagent/restagent.go", + "console": "integratedTerminal" + }, + { + "name": "debug sshsvc", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "d:/local.git/ems.agt/sshsvc/sshsvc.go", + "console": "integratedTerminal" + }, + { + "name": "debug loadpconf", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "d:/local.git/ems.agt/tools/loadpconf", + "args": ["-p","../../config/param/upf_param_config.yaml"], + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d844704 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "go.toolsEnvVars": { + "GOARCH": "amd64", + "GOOS": "windows" + }, + "go.testEnvVars": { + "GOARCH": "wasm", + "GOOS": "js" + }, + "commentTranslate.hover.enabled": true +} \ No newline at end of file diff --git a/README.md b/README.md index ad4b314..8c3dfa9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # ems_backend -临时仓库 \ No newline at end of file +后端 \ No newline at end of file diff --git a/features/aaaa/aaaa.go b/features/aaaa/aaaa.go new file mode 100644 index 0000000..8b10720 --- /dev/null +++ b/features/aaaa/aaaa.go @@ -0,0 +1,142 @@ +package aaaa + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "github.com/go-resty/resty/v2" + + "ems.agt/lib/dborm" + "ems.agt/lib/log" + "ems.agt/lib/oauth" + "ems.agt/lib/services" + "ems.agt/restagent/config" +) + +var ( + UriAAAASSO = config.DefaultUriPrefix + "/aaaa/{apiVersion}/security/sso" // for 4A external + + CustomUriAAAASSO = config.UriPrefix + "/aaaa/{apiVersion}/security/sso" // for 4A external +) + +var client = resty.New() + +func init() { + /* + client. + SetTimeout(10 * time.Second). + SetRetryCount(1). + SetRetryWaitTime(1 * time.Second). + SetRetryMaxWaitTime(2 * time.Second). + SetRetryAfter(func(client *resty.Client, resp *resty.Response) (time.Duration, error) { + return 0, errors.New("quota exceeded") + }) + */ + client.SetTimeout(3 * time.Second) +} + +type AAAATicket struct { + Ticket string `json:"ticket"` +} + +type SSOResult struct { + SSO struct { + Result string `json:"result"` + ResultMsg string `json:"result_msg"` + Ticket string `json:"ticket"` + ResultMsgcode string `json:"result_msgcode"` + Account []struct { + Accid string `json:"accid"` + } `json:"account"` + } `json:"sso"` +} + +// Get system state from NF/NFs +func GetSSOFromAAAA(w http.ResponseWriter, r *http.Request) { + log.Info("GetSSOFromAAAA processing... ") + + vars := r.URL.Query() + ticket := vars["ticket"] + if len(ticket) == 0 { + services.ResponseNotFound404UriNotExist(w, r) + return + } + log.Debug("ticket:", ticket) + + log.Debugf("r.RemoteAddr:%s r.Host: %s", r.RemoteAddr, r.Host) + + aaaaIp := r.RemoteAddr[:strings.Index(r.RemoteAddr, ":")] + omcIp := r.Host[:strings.Index(r.Host, ":")] + + log.Debugf("aaaaIp=%s omcIp=%s", aaaaIp, omcIp) + requestURI2NF := fmt.Sprintf("http://%s:8080/qryUserByTicket", aaaaIp) + + log.Debug("requestURI2NF:", requestURI2NF) + + aaaaTicket := &AAAATicket{ + Ticket: ticket[0], + } + + body, err := json.Marshal(aaaaTicket) + if err != nil { + log.Error("Failed to json.Marshal:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + response, err := client.R(). + EnableTrace(). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + SetBody(body). + Post(requestURI2NF) + if err != nil { + log.Error("Failed to Post:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + log.Debug("response:", response) + + switch response.StatusCode() { + case http.StatusOK, http.StatusCreated, http.StatusNoContent, http.StatusAccepted: + ssoResult := new(SSOResult) + json.Unmarshal(response.Body(), ssoResult) + var accid string + if len(ssoResult.SSO.Account) != 0 { + accid = ssoResult.SSO.Account[0].Accid + } + + log.Debug("accid:", accid) + exist, err := dborm.XormIsExistUser(accid) + if err != nil { + services.ResponseInternalServerError500ProcessError(w, err) + return + } + token := oauth.GenRandToken("aaaa") // Generate new token to session ID + affected, err := dborm.XormInsertSession(accid, r.RemoteAddr, token, + config.GetExpiresFromConfig(), config.GetYamlConfig().Auth.Session) + if err != nil { + log.Error("Failed to XormInsertSession:", err) + if affected == -1 { + services.ResponseForbidden403MultiLoginNotAllowed(w) + } else { + services.ResponseBadRequest400IncorrectLogin(w) + } + return + } + if exist == true { + redirectUrl := fmt.Sprintf("http://%s:8888/home.html?user=%s&token=%s", omcIp, accid, token) + services.ResponseRedirect(w, redirectUrl, accid, token) + return + } else { + services.ResponseBadRequest400IncorrectLogin(w) + return + } + default: + services.ResponseForbidden403NotPermission(w) + return + } +} diff --git a/features/cm/exec_linux.go b/features/cm/exec_linux.go new file mode 100644 index 0000000..3044649 --- /dev/null +++ b/features/cm/exec_linux.go @@ -0,0 +1,66 @@ +//go:build linux +// +build linux + +package cm + +import ( + "bytes" + "os/exec" + + "ems.agt/lib/log" +) + +func ExecCmd(command string) error { + log.Debug("Exec command:", command) + + cmd := exec.Command("/bin/bash", "-c", command) + out, err := cmd.CombinedOutput() + log.Tracef("Exec output: %v", string(out)) + if err != nil { + log.Error("exe cmd error: ", err) + return err + } + /* + if err := cmd.Start(); err != nil { + log.Error("Start error: ", err) + return err + } + if err := cmd.Wait(); err != nil { + log.Error("Wait error: ", err) + return err + } + */ + return nil +} + +func ExecShell(command string) error { + in := bytes.NewBuffer(nil) + cmd := exec.Command("sh") + cmd.Stdin = in + in.WriteString(command) + in.WriteString("exit\n") + if err := cmd.Start(); err != nil { + return err + } + return nil +} + +func ExecOsCmd(command, os string) error { + log.Debugf("Exec %s command:%s", os, command) + + var cmd *exec.Cmd + switch os { + case "Linux": + cmd = exec.Command(command) + case "Windows": + cmd = exec.Command("cmd", "/C", command) + } + + out, err := cmd.CombinedOutput() + log.Tracef("Exec output: %v", string(out)) + if err != nil { + log.Error("exe cmd error: ", err) + return err + } + return nil +} diff --git a/features/cm/exec_windows.go b/features/cm/exec_windows.go new file mode 100644 index 0000000..fa15045 --- /dev/null +++ b/features/cm/exec_windows.go @@ -0,0 +1,53 @@ +//go:build windows +// +build windows + +package cm + +import ( + "os/exec" + + "ems.agt/lib/log" +) + +func ExecCmd(command string) error { + log.Debug("Exec command:", command) + + cmd := exec.Command("cmd", "/C", command) + out, err := cmd.CombinedOutput() + log.Tracef("Exec output: %v", string(out)) + if err != nil { + log.Error("exe cmd error: ", err) + return err + } + /* + if err := cmd.Start(); err != nil { + log.Error("Start error: ", err) + return err + } + if err := cmd.Wait(); err != nil { + log.Error("Wait error: ", err) + return err + } + */ + return nil +} + +func ExecOsCmd(command, os string) error { + log.Debugf("Exec %s command:%s", os, command) + + var cmd *exec.Cmd + switch os { + case "Linux": + cmd = exec.Command(command) + case "Windows": + cmd = exec.Command("cmd", "/C", command) + } + + out, err := cmd.CombinedOutput() + log.Tracef("Exec output: %v", string(out)) + if err != nil { + log.Error("exe cmd error: ", err) + return err + } + return nil +} diff --git a/features/cm/license.go b/features/cm/license.go new file mode 100644 index 0000000..e691a90 --- /dev/null +++ b/features/cm/license.go @@ -0,0 +1,151 @@ +package cm + +import ( + "net/http" + + "ems.agt/lib/log" + "ems.agt/lib/services" + "ems.agt/restagent/config" + + "github.com/gorilla/mux" +) + +var ( + // License + LicenseUri = config.DefaultUriPrefix + "/systemManagement/{apiVersion}/{neType}/license" + NeLicenseUri = config.DefaultUriPrefix + "/systemManagement/{apiVersion}/{neType}/license/{neId}" + + CustomLicenseUri = config.UriPrefix + "/systemManagement/{apiVersion}/{neType}/license" + CustomNeLicenseUri = config.UriPrefix + "/systemManagement/{apiVersion}/{neType}/license/{neId}" +) + +func UploadLicenseFile(w http.ResponseWriter, r *http.Request) { + log.Debug("UploadLicenseFile processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Http request error:", err) + return + } + + vars := mux.Vars(r) + neType := vars["neType"] + if neType == "" { + log.Error("neType is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + // neTypeUpper := strings.ToUpper(neType) + // neTypeLower := strings.ToLower(neType) + + services.ResponseStatusOK204NoContent(w) + return +} + +func DownloadLicenseFile(w http.ResponseWriter, r *http.Request) { + log.Debug("DownloadLicenseFile processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + vars := mux.Vars(r) + neType := vars["neType"] + if neType == "" { + log.Error("neType is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + // // neTypeUpper := strings.ToUpper(neType) + // //neTypeLower := strings.ToLower(neType) + + // version := vars["version"] + // if version == "" { + // log.Error("version is empty") + // services.ResponseNotFound404UriNotExist(w, r) + // return + // } + + // sql := fmt.Sprintf("select * from ne_software where ne_type='%s' and version='%s'", neTypeUpper, version) + // neSoftware, err := dborm.XormGetDataBySQL(sql) + // if err != nil { + // log.Error("Faile to XormGetDataBySQL:", err) + // services.ResponseInternalServerError500ProcessError(w, err) + // return + // } else if len(*neSoftware) == 0 { + // err := global.ErrCMNotFoundTargetSoftware + // log.Error(err) + // services.ResponseInternalServerError500ProcessError(w, err) + // return + // } + + // fileName := (*neSoftware)[0]["file_name"] + // path := (*neSoftware)[0]["path"] + // md5Sum := (*neSoftware)[0]["md5_sum"] + + // services.ResponseFileWithNameAndMD5(w, http.StatusOK, fileName, path, md5Sum) + return +} + +func DeleteLcenseFile(w http.ResponseWriter, r *http.Request) { + log.Debug("DeleteLcenseFile processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + vars := mux.Vars(r) + neType := vars["neType"] + if neType == "" { + log.Error("neType is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + // neTypeUpper := strings.ToUpper(neType) + // //neTypeLower := strings.ToLower(neType) + + // version := vars["version"] + // if version == "" { + // log.Error("version is empty") + // services.ResponseNotFound404UriNotExist(w, r) + // return + // } + + // sql := fmt.Sprintf("select * from ne_software where ne_type='%s' and version='%s'", neTypeUpper, version) + // neSoftware, err := dborm.XormGetDataBySQL(sql) + // if err != nil { + // log.Error("Faile to XormGetDataBySQL:", err) + // services.ResponseInternalServerError500ProcessError(w, err) + // return + // } else if len(*neSoftware) == 0 { + // err := global.ErrCMNotFoundTargetSoftware + // log.Error(err) + // services.ResponseInternalServerError500ProcessError(w, err) + // return + // } + + // where := fmt.Sprintf("ne_type='%s' and version='%s'", neTypeUpper, version) + // affected, err := dborm.XormDeleteDataByWhere(where, "ne_software") + // if err != nil || affected == 0 { + // log.Error("Faile to XormGetDataBySQL:", err) + // services.ResponseInternalServerError500ProcessError(w, err) + // return + // } + + // fileName := (*neSoftware)[0]["file_name"] + // path := (*neSoftware)[0]["path"] + // filePath := fmt.Sprintf("%s/%s", path, fileName) + // err = os.Remove(filePath) + // if err != nil { + // log.Error("Faile to Remove:", err) + // services.ResponseInternalServerError500ProcessError(w, err) + // return + // } + + services.ResponseStatusOK204NoContent(w) + return +} diff --git a/features/cm/ne.go b/features/cm/ne.go new file mode 100644 index 0000000..3ecf8eb --- /dev/null +++ b/features/cm/ne.go @@ -0,0 +1,906 @@ +package cm + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "strings" + "time" + + "ems.agt/lib/dborm" + "ems.agt/lib/global" + "ems.agt/lib/log" + "ems.agt/lib/services" + "ems.agt/restagent/config" + + "github.com/go-resty/resty/v2" + "github.com/gorilla/mux" +) + +var ( + UriParamOmcNeConfig = config.DefaultUriPrefix + "/systemManagement/v1/elementType/%s/objectType/config/omcNeConfig" + // NE CM export/import + NeCmUri = config.DefaultUriPrefix + "/systemManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/cm" + // NE info + UriNeInfo = config.DefaultUriPrefix + "/systemManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/neInfo" + // NE backup file + UriNeCmFile = config.DefaultUriPrefix + "/systemManagement/{apiVersion}/{neType}/neBackup/{fileName}" + // service action uri, action: start/stop/restart + UriNeService = config.DefaultUriPrefix + "/systemManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/service/{action}" + // nf instance action uri, action: start/stop/restart + UriNeInstance = config.DefaultUriPrefix + "/systemManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/instance/{action}" + + CustomUriParamOmcNeConfig = config.UriPrefix + "/systemManagement/v1/elementType/%s/objectType/config/omcNeConfig" + CustomNeCmUri = config.UriPrefix + "/systemManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/cm" + CustomUriNeInfo = config.UriPrefix + "/systemManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/neInfo" + CustomUriNeCmFile = config.UriPrefix + "/systemManagement/{apiVersion}/{neType}/neBackup/{fileName}" + // service action uri, action: start/stop/restart + CustomUriNeService = config.UriPrefix + "/systemManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/service/{action}" + // nf instance action uri, action: start/stop/restart + CustomUriNeInstance = config.UriPrefix + "/systemManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/instance/{action}" +) + +func init() { + +} + +func GetNeInfo(w http.ResponseWriter, r *http.Request) { + log.Debug("GetNeInfo processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + vars := mux.Vars(r) + neType := vars["elementTypeValue"] + if neType == "" { + log.Error("elementTypeValue is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + neId := services.GetUriParamString(r, "ne_id", ",", false, false) + + // no, _ := strconv.ParseInt(neId, 10, 64) + neInfo, err := dborm.XormGetNeInfo(neType, neId) + if err != nil { + log.Error("dborm.XormGetNeInfo is failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + + var response services.DataResponse + response.Data = neInfo + services.ResponseWithJson(w, http.StatusOK, response) +} + +type OmcNeConfig struct { + NeId string `json:"neId" xorm:"ne_id"` // 网元标识(内部), + RmUID string `json:"rmUID" xorm:"rm_uid"` // rmUID 网元唯一标识 + NeName string `json:"neName" xorm:"ne_name"` // 网元名称(内部)/友好名称(北向资源/性能等使用) + PvFlag string `json:"pvFlag" xorm:"pv_flag"` // 网元虚实性标识 VNF/PNF: 虚拟/物理 + Province string `json:"province" xorm:"province"` // 网元所在省份 + VendorName string `json:"vendorName" xorm:"vendor_name"` // 厂商名称 + // ManagedBy string `json:"managedBy" xorm:"managed_by"` // 管理ManagedElement的ManagementNode对象类的DN值 + Dn string `json:"dn" xorm:"dn"` // 资源里边的ManagedBy,性能的Dn,网络唯一标识 +} + +func PostNeInfo(w http.ResponseWriter, r *http.Request) { + log.Debug("PostNeInfo processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + vars := mux.Vars(r) + neType := vars["elementTypeValue"] + if neType == "" { + log.Error("elementTypeValue is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + body, err := io.ReadAll(io.LimitReader(r.Body, global.RequestBodyMaxLen)) + if err != nil { + log.Error("io.ReadAll is failed:", err) + services.ResponseNotFound404UriNotExist(w, r) + return + } + log.Debug("Body:", string(body)) + + neInfo := new(dborm.NeInfo) + err = json.Unmarshal(body, neInfo) + if err != nil { + log.Error("Failed to json.Unmarshal:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + neInfo.UpdateTime = time.Now().Format(time.DateTime) + log.Debug("NE info:", neInfo) + + hostUri := global.CombineHostUri(neInfo.Ip, neInfo.Port) + //hostUri := fmt.Sprintf("http://%s:%v", neInfo.Ip, neInfo.Port) + apiUri := fmt.Sprintf(UriParamOmcNeConfig, strings.ToLower(neInfo.NeType)) + requestURI2NF := fmt.Sprintf("%s%s", hostUri, apiUri) + log.Debug("requestURI2NF:", requestURI2NF) + + omcNeConfig := &OmcNeConfig{ + NeId: neInfo.NeId, + RmUID: neInfo.RmUID, + NeName: neInfo.NeName, + PvFlag: neInfo.PvFlag, + Province: neInfo.Province, + VendorName: neInfo.VendorName, + Dn: neInfo.Dn, + } + body, _ = json.Marshal(omcNeConfig) + client := resty.New() + response, err := client.R(). + EnableTrace(). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + SetBody(body). + Put(requestURI2NF) + if err != nil { + log.Error("Failed to Put:", err) + services.ResponseInternalServerError500NFConnectRefused(w) + return + } + log.Info("StatusCode: ", response.StatusCode()) + + if !config.GetYamlConfig().OMC.Chk2Ne { + affected, err := dborm.XormInsertNeInfo(neInfo) + if err != nil { + log.Error("Failed to insert Ne info:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + + mapRow := make(map[string]interface{}) + row := map[string]interface{}{"affectedRows": affected} + mapRow["data"] = row + services.ResponseWithJson(w, http.StatusOK, mapRow) + return + } else { + respMsg := make(map[string]interface{}) + switch response.StatusCode() { + case http.StatusOK, http.StatusCreated, http.StatusNoContent, http.StatusAccepted: + affected, err := dborm.XormInsertNeInfo(neInfo) + if err != nil { + log.Error("Failed to dborm.XormInsertNeInfo:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } else if affected <= 0 { + log.Infof("Not record affected to insert ne_info") + } + services.ResponseStatusOK204NoContent(w) + return + default: + log.Info("response body:", string(response.Body())) + body := new(map[string]interface{}) + _ = json.Unmarshal(response.Body(), &body) + respMsg["error"] = body + } + + services.ResponseWithJson(w, response.StatusCode(), respMsg) + return + } +} + +func PutNeInfo(w http.ResponseWriter, r *http.Request) { + log.Debug("PutNeInfo processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + vars := mux.Vars(r) + neType := vars["elementTypeValue"] + if neType == "" { + log.Error("elementTypeValue is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + body, err := io.ReadAll(io.LimitReader(r.Body, global.RequestBodyMaxLen)) + if err != nil { + log.Error("io.ReadAll is failed:", err) + services.ResponseNotFound404UriNotExist(w, r) + return + } + + neInfo := new(dborm.NeInfo) + _ = json.Unmarshal(body, neInfo) + neInfo.NeType = strings.ToUpper(neType) + neInfo.UpdateTime = time.Now().Format(time.DateTime) + log.Debug("NE info:", neInfo) + + hostUri := global.CombineHostUri(neInfo.Ip, neInfo.Port) + //hostUri := fmt.Sprintf("http://%s:%v", neInfo.Ip, neInfo.Port) + apiUri := fmt.Sprintf(UriParamOmcNeConfig, strings.ToLower(neType)) + requestURI2NF := fmt.Sprintf("%s%s", hostUri, apiUri) + log.Debug("requestURI2NF:", requestURI2NF) + + omcNeConfig := &OmcNeConfig{ + NeId: neInfo.NeId, + RmUID: neInfo.RmUID, + NeName: neInfo.NeName, + PvFlag: neInfo.PvFlag, + Province: neInfo.Province, + VendorName: neInfo.VendorName, + Dn: neInfo.Dn, + } + body, _ = json.Marshal(omcNeConfig) + client := resty.New() + response, err := client.R(). + EnableTrace(). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + SetBody(body). + Put(requestURI2NF) + if err != nil { + log.Error("Failed to Put:", err) + services.ResponseInternalServerError500NFConnectRefused(w) + return + } + log.Info("StatusCode: ", response.StatusCode()) + + if !config.GetYamlConfig().OMC.Chk2Ne { + affected, err := dborm.XormUpdateNeInfo(neInfo) + if err != nil { + log.Error("Failed to update Ne info:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + + mapRow := make(map[string]interface{}) + row := map[string]interface{}{"affectedRows": affected} + mapRow["data"] = row + services.ResponseWithJson(w, http.StatusOK, mapRow) + return + } else { + respMsg := make(map[string]interface{}) + switch response.StatusCode() { + case http.StatusOK, http.StatusCreated, http.StatusNoContent, http.StatusAccepted: + affected, err := dborm.XormUpdateNeInfo(neInfo) + if err != nil { + log.Error("Failed to dborm.XormUpdateNeInfo:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } else if affected <= 0 { + log.Infof("Not record affected to insert ne_info") + } + services.ResponseStatusOK204NoContent(w) + return + default: + log.Info("response body:", string(response.Body())) + body := new(map[string]interface{}) + _ = json.Unmarshal(response.Body(), &body) + respMsg["error"] = body + } + + services.ResponseWithJson(w, response.StatusCode(), respMsg) + return + } +} + +func DeleteNeInfo(w http.ResponseWriter, r *http.Request) { + log.Debug("DeleteNeInfo processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + vars := mux.Vars(r) + neType := vars["elementTypeValue"] + if neType == "" { + log.Error("elementTypeValue is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + + body, err := io.ReadAll(io.LimitReader(r.Body, global.RequestBodyMaxLen)) + if err != nil { + log.Error("io.ReadAll is failed:", err) + services.ResponseNotFound404UriNotExist(w, r) + return + } + + neInfo := new(dborm.NeInfo) + _ = json.Unmarshal(body, neInfo) + neInfo.NeType = strings.ToUpper(neType) + neInfo.NeId = services.GetUriParamString(r, "ne_id", ",", false, false) + neInfo, err = dborm.XormGetNeInfo(neInfo.NeType, neInfo.NeId) + if err != nil || neInfo == nil { + log.Error("Failed to delete Ne info:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + log.Debug("NE info:", neInfo) + + // if NE in active status, can't delete NE + if !IsActiveNF(neInfo) { + affected, err := dborm.XormDeleteNeInfo(neInfo) + if err != nil { + log.Error("Failed to delete Ne info:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + + mapRow := make(map[string]interface{}) + row := map[string]interface{}{"affectedRows": affected} + mapRow["data"] = row + services.ResponseWithJson(w, http.StatusOK, mapRow) + return + } + err = global.ErrCMCannotDeleteActiveNE + log.Error(err) + services.ResponseInternalServerError500ProcessError(w, err) +} + +func IsActiveNF(neInfo *dborm.NeInfo) bool { + log.Debug("IsActiveNF processing... ") + + hostUri := fmt.Sprintf("http://%s:%v", neInfo.Ip, neInfo.Port) + requestURI := fmt.Sprintf(config.UriPrefix+"/systemManagement/v1/elementType/%s/objectType/systemState", + strings.ToLower(neInfo.NeType)) + + client := resty.New() + response, err := client.R(). + EnableTrace(). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + Get(hostUri + requestURI) + if err != nil { + log.Error("Failed to Get:", err) + } + + switch response.StatusCode() { + case http.StatusOK, http.StatusCreated, http.StatusNoContent, http.StatusAccepted: + return true + } + return false +} + +func ExportCmFromNF(w http.ResponseWriter, r *http.Request) { + log.Debug("ExportCmFromNF processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + vars := mux.Vars(r) + neType := vars["elementTypeValue"] + if neType == "" { + log.Error("elementTypeValue is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + neTypeUpper := strings.ToUpper(neType) + neTypeLower := strings.ToLower(neType) + + neId := services.GetUriParamString(r, "ne_id", ",", false, false) + + // neInfo := new(dborm.NeInfo) + neInfo, err := dborm.XormGetNeInfo(neType, neId) + if err != nil { + log.Errorf("Failed to get ne_info:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + log.Debug("neInfo:", neInfo) + nePath := fmt.Sprintf("%s/etc/%s", config.GetYamlConfig().OMC.Backup, neTypeLower) + isExist, err := global.PathExists(nePath) + if err != nil { + log.Errorf("Failed to stat:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + if isExist { + err = os.RemoveAll(nePath) + if err != nil { + log.Errorf("Failed to RemoveAll:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + } + err = os.MkdirAll(nePath, os.ModePerm) + if err != nil { + log.Errorf("Failed to MkdirAll:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + var scpCmd string + ipType := global.ParseIPAddr(neInfo.Ip) + if neTypeLower != "omc" { + if ipType == global.IsIPv4 { + scpCmd = fmt.Sprintf("scp -r %s@%s:%s/%s/*.yaml %s/etc/%s", config.GetYamlConfig().NE.User, + neInfo.Ip, config.GetYamlConfig().NE.EtcDir, + neTypeLower, config.GetYamlConfig().OMC.Backup, neTypeLower) + } else { + scpCmd = fmt.Sprintf("scp -r %s@[%s]:%s/%s/*.yaml %s/etc/%s", config.GetYamlConfig().NE.User, + neInfo.Ip, config.GetYamlConfig().NE.EtcDir, + neTypeLower, config.GetYamlConfig().OMC.Backup, neTypeLower) + } + } else { + if ipType == global.IsIPv4 { + scpCmd = fmt.Sprintf("scp -r %s@%s:%s/etc/*.yaml %s/etc/%s", config.GetYamlConfig().NE.User, + neInfo.Ip, config.GetYamlConfig().NE.OmcDir, config.GetYamlConfig().OMC.Backup, neTypeLower) + } else { + scpCmd = fmt.Sprintf("scp -r %s@[%s]:%s/etc/*.yaml %s/etc/%s", config.GetYamlConfig().NE.User, + neInfo.Ip, config.GetYamlConfig().NE.OmcDir, config.GetYamlConfig().OMC.Backup, neTypeLower) + } + } + + zipFile := fmt.Sprintf("%s-%s-etc-%s.zip", neTypeLower, strings.ToLower(neInfo.NeId), time.Now().Format(global.DateData)) + zipFilePath := config.GetYamlConfig().OMC.Backup + "/" + zipFile + zipCmd := fmt.Sprintf("cd %s/etc && zip -r %s %s/*", config.GetYamlConfig().OMC.Backup, zipFilePath, neTypeLower) + + command := fmt.Sprintf("%s&&%s", scpCmd, zipCmd) + + log.Debug("command:", command) + err = ExecCmd(command) + if err != nil { + log.Error("Faile to exec command:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + md5Sum, err := global.GetFileMD5Sum(zipFilePath) + if err != nil { + log.Error("Faile to md5sum:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + //log.Debug("md5Str:", md5Sum) + path := config.GetYamlConfig().OMC.Backup + neBackup := dborm.NeBackup{NeType: neTypeUpper, NeId: neId, FileName: zipFile, Path: path, Md5Sum: md5Sum} + _, err = dborm.XormInsertTableOne("ne_backup", neBackup) + if err != nil { + log.Error("Faile to XormInsertTableOne:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + //services.ResponseFileWithNameAndMD5(w, http.StatusOK, zipFile, path, md5Sum) + services.ResponseStatusOK204NoContent(w) +} + +type ImportCMJson struct { + FileName string `json:"fileName"` +} + +func ImportCmToNF(w http.ResponseWriter, r *http.Request) { + log.Debug("ImportCmToNF processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + vars := mux.Vars(r) + neType := vars["elementTypeValue"] + if neType == "" { + log.Error("elementTypeValue is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + neTypeUpper := strings.ToUpper(neType) + neTypeLower := strings.ToLower(neType) + + neId := services.GetUriParamString(r, "ne_id", ",", false, false) + + var fileName, path string + if services.IsJsonContentType(r) { + body, err := io.ReadAll(io.LimitReader(r.Body, global.RequestBodyMaxLen)) + if err != nil { + log.Error("io.ReadAll is failed:", err) + services.ResponseNotFound404UriNotExist(w, r) + return + } + log.Debug("Body:", string(body)) + + importCMJson := new(ImportCMJson) + _ = json.Unmarshal(body, importCMJson) + fileName = importCMJson.FileName + path = config.GetYamlConfig().OMC.Backup + } else { + path = config.GetYamlConfig().OMC.Upload + fileName, err = services.HandleUploadFile(r, path, "") + if err != nil { + log.Error("Faile to HandleUploadFile:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + } + filePath := fmt.Sprintf("%s/%s", path, fileName) + + // neInfo := new(dborm.NeInfo) + neInfo, err := dborm.XormGetNeInfo(neType, neId) + if err != nil { + log.Errorf("Failed to get ne_info:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + log.Debug("neInfo:", neInfo) + + md5Sum, err := global.GetFileMD5Sum(filePath) + if err != nil { + log.Error("Faile to GetFileMD5Sum:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + //neBackup := dborm.NeBackup{NeType: neType, NeId: neId, Md5Sum: md5Sum} + //log.Debug("neBackup:", neBackup) + where := fmt.Sprintf("ne_type='%s' and ne_id='%s' and md5_sum='%s'", neTypeUpper, neId, md5Sum) + has, err := dborm.XormExistTableOne("ne_backup", where) + if err != nil { + log.Error("Faile to XormInsertTableOne:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } else if !has { + err = global.ErrCMInvalidBackupFile + log.Error(err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + // nePath := fmt.Sprintf("%s/etc/%s", config.GetYamlConfig().OMC.Upload, neTypeLower) + // isExist, err := global.PathExists(nePath) + // if err != nil { + // log.Errorf("Failed to stat:", err) + // services.ResponseInternalServerError500ProcessError(w, err) + // return + // } + // if isExist { + // err = os.RemoveAll(nePath) + // if err != nil { + // log.Errorf("Failed to remove:", err) + // services.ResponseInternalServerError500ProcessError(w, err) + // return + // } + // } + // unzipCmd := fmt.Sprintf("unzip -o %s -d %s", filePath, config.GetYamlConfig().OMC.Upload) + + // var scpCmd string + // ipType := global.ParseIPAddr(neInfo.Ip) + // if ipType == global.IsIPv4 { + // scpCmd = fmt.Sprintf("scp -r %s/etc/%s %s@%s:%s", config.GetYamlConfig().OMC.Upload, + // neTypeLower, config.GetYamlConfig().NE.User, neInfo.Ip, config.GetYamlConfig().NE.EtcDir) + // } else { + // scpCmd = fmt.Sprintf("scp -r %s/etc/%s %s@[%s]:%s", config.GetYamlConfig().OMC.Upload, + // neTypeLower, config.GetYamlConfig().NE.User, neInfo.Ip, config.GetYamlConfig().NE.EtcDir) + // } + + // err = ExecCmd(fmt.Sprintf("%s && %s", unzipCmd, scpCmd)) + // if err != nil { + // log.Errorf("Faile to scp NF: neType=%s, neId=%s, ip=%s", neType, neId, neInfo.Ip) + // services.ResponseInternalServerError500ProcessError(w, err) + // return + // } + + // nePath := fmt.Sprintf("%s/etc/%s", config.GetYamlConfig().OMC.Upload, neTypeLower) + // isExist, err := global.PathExists(nePath) + // if err != nil { + // log.Errorf("Failed to stat:", err) + // services.ResponseInternalServerError500ProcessError(w, err) + // return + // } + // if isExist { + // err = os.RemoveAll(nePath) + // if err != nil { + // log.Errorf("Failed to remove:", err) + // services.ResponseInternalServerError500ProcessError(w, err) + // return + // } + // } + var scpZipCmd string + ipType := global.ParseIPAddr(neInfo.Ip) + if ipType == global.IsIPv4 { + scpZipCmd = fmt.Sprintf("scp -r %s %s@%s:%s", filePath, + config.GetYamlConfig().NE.User, neInfo.Ip, config.GetYamlConfig().NE.ScpDir) + } else { + scpZipCmd = fmt.Sprintf("scp -r %s %s@[%s]:%s", filePath, + config.GetYamlConfig().NE.User, neInfo.Ip, config.GetYamlConfig().NE.ScpDir) + } + err = ExecCmd(scpZipCmd) + if err != nil { + log.Errorf("Faile to scp NF: neType=%s, neId=%s, ip=%s", neType, neId, neInfo.Ip) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + neFilePath := config.GetYamlConfig().NE.ScpDir + "/" + fileName + var unzipCmd string + if neTypeLower != "omc" { + unzipCmd = fmt.Sprintf("sudo unzip -o %s -d %s", neFilePath, config.GetYamlConfig().NE.EtcDir) + } else { + unzipCmd = fmt.Sprintf("sudo unzip -oj %s -d %s/etc", neFilePath, config.GetYamlConfig().NE.OmcDir) + } + sshHost := fmt.Sprintf("%s@%s", config.GetYamlConfig().NE.User, neInfo.Ip) + cmd := exec.Command("ssh", sshHost, unzipCmd) + out, err := cmd.CombinedOutput() + log.Tracef("Exec output: %v", string(out)) + if err != nil { + log.Error("Faile to execute rpm command:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + services.ResponseStatusOK204NoContent(w) +} + +func DownloadNeBackupFile(w http.ResponseWriter, r *http.Request) { + log.Debug("DownloadNeBackupFile processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + vars := mux.Vars(r) + neType := vars["neType"] + if neType == "" { + log.Error("neType is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + neTypeUpper := strings.ToUpper(neType) + //neTypeLower := strings.ToLower(neType) + + fileName := vars["fileName"] + if fileName == "" { + log.Error("fileName is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + + sql := fmt.Sprintf("select * from ne_backup where ne_type='%s' and file_name='%s'", neTypeUpper, fileName) + neBackup, err := dborm.XormGetDataBySQL(sql) + if err != nil { + log.Error("Faile to XormGetDataBySQL:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } else if len(*neBackup) == 0 { + err := global.ErrCMNotFoundTargetBackupFile + log.Error(err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + path := (*neBackup)[0]["path"] + md5Sum := (*neBackup)[0]["md5_sum"] + + services.ResponseFileWithNameAndMD5(w, http.StatusOK, fileName, path, md5Sum) +} + +func DeleteNeBackupFile(w http.ResponseWriter, r *http.Request) { + log.Debug("DeleteNeBackupFile processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + vars := mux.Vars(r) + neType := vars["neType"] + if neType == "" { + log.Error("neType is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + neTypeUpper := strings.ToUpper(neType) + //neTypeLower := strings.ToLower(neType) + + fileName := vars["fileName"] + if fileName == "" { + log.Error("fileName is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + + sql := fmt.Sprintf("select * from ne_backup where ne_type='%s' and file_name='%s'", neTypeUpper, fileName) + neBackup, err := dborm.XormGetDataBySQL(sql) + if err != nil { + log.Error("Faile to XormGetDataBySQL:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } else if len(*neBackup) == 0 { + err := global.ErrCMNotFoundTargetBackupFile + log.Error(err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + where := fmt.Sprintf("ne_type='%s' and file_name='%s'", neTypeUpper, fileName) + affected, err := dborm.XormDeleteDataByWhere(where, "ne_backup") + if err != nil || affected == 0 { + log.Error("Faile to XormGetDataBySQL:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + path := (*neBackup)[0]["path"] + filePath := fmt.Sprintf("%s/%s", path, fileName) + err = os.Remove(filePath) + if err != nil { + log.Error("Faile to Remove:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + services.ResponseStatusOK204NoContent(w) +} + +func PostNeServiceAction(w http.ResponseWriter, r *http.Request) { + log.Debug("PostNeServiceAction processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + vars := mux.Vars(r) + neType := vars["elementTypeValue"] + if neType == "" { + log.Error("elementTypeValue is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + //neTypeUpper := strings.ToUpper(neType) + neTypeLower := strings.ToLower(neType) + action := vars["action"] + + neId := services.GetUriParamString(r, "ne_id", ",", false, false) + + // neInfo := new(dborm.NeInfo) + neInfo, err := dborm.XormGetNeInfo(neType, neId) + if err != nil { + log.Error("Failed to get ne_info:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + log.Debug("neInfo:", neInfo) + + sshHost := fmt.Sprintf("%s@%s", config.GetYamlConfig().NE.User, neInfo.Ip) + switch neTypeLower { + case "omc": + actionCmd := fmt.Sprintf("sudo %s/bin/omcsvc.sh %s", config.GetYamlConfig().NE.OmcDir, action) + cmd := exec.Command("ssh", sshHost, actionCmd) + out, err := cmd.CombinedOutput() + log.Debugf("Exec output: %v", string(out)) + if err != nil { + log.Errorf("Faile to execute ssh %s omc:%v", action, err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + case "ims": + switch action { + case "start", "stop": + actionCmd := fmt.Sprintf("sudo ims-%s", action) + cmd := exec.Command("ssh", sshHost, actionCmd) + out, err := cmd.CombinedOutput() + log.Debugf("Exec output: %v", string(out)) + if err != nil { + log.Errorf("Faile to execute %s command:%v", actionCmd, err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + case "restart": + actionCmd := "sudo ims-stop && sudo ims-start" + cmd := exec.Command("ssh", sshHost, actionCmd) + out, err := cmd.CombinedOutput() + log.Debugf("Exec output: %v", string(out)) + if err != nil { + log.Errorf("Faile to execute %s command:%v", actionCmd, err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + default: + err = global.ErrCMUnknownServiceAction + log.Errorf("%v, action:%s", err, action) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + default: + actionCmd := fmt.Sprintf("sudo systemctl %s %s.service", action, neTypeLower) + cmd := exec.Command("ssh", sshHost, actionCmd) + out, err := cmd.CombinedOutput() + log.Debugf("Exec output: %v", string(out)) + if err != nil { + log.Error("Faile to execute command:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + } + + services.ResponseStatusOK204NoContent(w) +} + +func PostNeInstanceAction(w http.ResponseWriter, r *http.Request) { + log.Debug("PostNeInstanceAction processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + vars := mux.Vars(r) + neType := vars["elementTypeValue"] + if neType == "" { + log.Error("elementTypeValue is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + //neTypeUpper := strings.ToUpper(neType) + //neTypeLower := strings.ToLower(neType) + action := vars["action"] + + neId := services.GetUriParamString(r, "ne_id", ",", false, false) + + // neInfo := new(dborm.NeInfo) + neInfo, err := dborm.XormGetNeInfo(neType, neId) + if err != nil { + log.Errorf("Failed to get ne_info:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + log.Debug("neInfo:", neInfo) + + sshHost := fmt.Sprintf("%s@%s", config.GetYamlConfig().NE.User, neInfo.Ip) + switch action { + case "poweron": + actionCmd := "sudo poweron" + cmd := exec.Command("ssh", sshHost, actionCmd) + out, err := cmd.CombinedOutput() + log.Debugf("Exec output: %v", string(out)) + if err != nil { + log.Error("Faile to execute ssh %s omc:", action, err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + case "poweroff": + actionCmd := "sudo poweroff" + cmd := exec.Command("ssh", sshHost, actionCmd) + out, err := cmd.CombinedOutput() + log.Debugf("Exec output: %v", string(out)) + if err != nil { + log.Error("Faile to execute ssh sudo poweroff:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + case "reboot": + actionCmd := "sudo reboot" + cmd := exec.Command("ssh", sshHost, actionCmd) + out, err := cmd.CombinedOutput() + log.Debugf("Exec output: %v", string(out)) + if err != nil { + log.Error("Faile to execute ssh sudo reboot:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + default: + err = global.ErrCMUnknownInstanceAction + log.Errorf("%v, action:%s", err, action) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + services.ResponseStatusOK204NoContent(w) +} diff --git a/features/cm/param.go b/features/cm/param.go new file mode 100644 index 0000000..d273661 --- /dev/null +++ b/features/cm/param.go @@ -0,0 +1,242 @@ +package cm + +import ( + "ems.agt/lib/dborm" + "ems.agt/lib/global" + "ems.agt/lib/log" + "ems.agt/lib/services" + "ems.agt/restagent/config" + + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/go-resty/resty/v2" + "github.com/gorilla/mux" +) + +var ( + // parameter config management + ParamConfigUri = config.DefaultUriPrefix + "/systemManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/config/{paraName}" + + CustomParamConfigUri = config.UriPrefix + "/systemManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/config/{paraName}" +) + +func GetParamConfigFromNF(w http.ResponseWriter, r *http.Request) { + log.Debug("GetParamConfigFromNF processing... ") + + // data := make([]map[string]interface{}, 1) + var response services.DataResponse + vars := mux.Vars(r) + neType := vars["elementTypeValue"] + if neType == "" { + log.Error("elementTypeValue is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + + token, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + restHostPort := fmt.Sprintf("http://127.0.0.1:%d", config.GetYamlConfig().Rest[0].Port) + getNeInfoPattern := fmt.Sprintf(config.UriPrefix+"/databaseManagement/v1/%s/ne_info", config.GetYamlConfig().Database.Name) + getNeInfoURI := restHostPort + getNeInfoPattern + neId := services.GetUriParamString(r, "ne_id", ",", true, false) + if neId == "" { + getNeInfoURI = getNeInfoURI + fmt.Sprintf("?WHERE=status='0'+and+ne_type='%s'", neType) + } else { + getNeInfoURI = getNeInfoURI + fmt.Sprintf("?WHERE=status='0'+and+ne_type='%v'+and+ne_id+in+%v", neType, neId) + } + log.Debug("getNeInfoURI:", getNeInfoURI) + + client := resty.New() + resp, err := client.R(). + EnableTrace(). + SetHeaders(map[string]string{"accessToken": token}). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + Get(getNeInfoURI) + if err != nil { + log.Error("Get from database is failure!") + services.ResponseInternalServerError500NFConnectRefused(w) + return + } + log.Debug("NE info:", string(resp.Body())) + + // var neList []dborm.NeInfo + neList, _ := dborm.XormParseResult(resp.Body()) + + if len(neList) >= 1 { + s := neList[0] + requestURI2NF := fmt.Sprintf("http://%s:%v%s", s.Ip, s.Port, r.RequestURI) + log.Debug("requestURI2NF:", requestURI2NF) + + resp, err := client.R(). + EnableTrace(). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + Get(requestURI2NF) + if err != nil { + log.Error("Failed to Get from NF:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } else { + _ = json.Unmarshal(resp.Body(), &response) + } + log.Debug("response:", response) + } + + services.ResponseWithJson(w, http.StatusOK, response) +} + +func PostParamConfigToNF(w http.ResponseWriter, r *http.Request) { + log.Debug("PostParamConfigToNF processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + vars := mux.Vars(r) + neType := vars["elementTypeValue"] + if neType == "" { + log.Error("elementTypeValue is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + neId := services.GetUriParamString(r, "ne_id", ",", false, false) + + // no, _ := strconv.ParseInt(neId, 10, 64) + neInfo, err := dborm.XormGetNeInfo(neType, neId) + if err != nil { + log.Error("dborm.XormGetNeInfo is failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + + requestURI2NF := fmt.Sprintf("http://%s:%v%s", neInfo.Ip, neInfo.Port, r.RequestURI) + log.Debug("requestURI2NF: POST ", requestURI2NF) + + body, err := io.ReadAll(io.LimitReader(r.Body, global.RequestBodyMaxLen)) //io.LimitReader限制大小 + if err != nil { + log.Error("io.ReadAll is failed:", err) + services.ResponseNotFound404UriNotExist(w, r) + return + } + + client := resty.New() + response, err := client.R(). + EnableTrace(). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + SetBody(body). + Post(requestURI2NF) + if err != nil { + log.Error("Failed to POST to NF:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + services.ResponseWithJson(w, http.StatusNoContent, response) +} + +func PutParamConfigToNF(w http.ResponseWriter, r *http.Request) { + log.Debug("PutParamConfigToNF processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + vars := mux.Vars(r) + neType := vars["elementTypeValue"] + if neType == "" { + log.Error("elementTypeValue is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + neId := services.GetUriParamString(r, "ne_id", ",", false, false) + + // no, _ := strconv.ParseInt(neId, 10, 64) + neInfo, err := dborm.XormGetNeInfo(neType, neId) + if err != nil { + log.Error("dborm.XormGetNeInfo is failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + + requestURI2NF := fmt.Sprintf("http://%s:%v%s", neInfo.Ip, neInfo.Port, r.RequestURI) + log.Debug("requestURI2NF: PUT ", requestURI2NF) + + body, err := io.ReadAll(io.LimitReader(r.Body, global.RequestBodyMaxLen)) //io.LimitReader限制大小 + if err != nil { + log.Error("io.ReadAll is failed:", err) + services.ResponseNotFound404UriNotExist(w, r) + return + } + + client := resty.New() + response, err := client.R(). + EnableTrace(). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + SetBody(body). + Put(requestURI2NF) + if err != nil { + log.Error("Failed to Put to NF:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + services.ResponseWithJson(w, http.StatusNoContent, response) +} + +func DeleteParamConfigToNF(w http.ResponseWriter, r *http.Request) { + log.Debug("DeleteParamConfigToNF processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + vars := mux.Vars(r) + neType := vars["elementTypeValue"] + if neType == "" { + log.Error("elementTypeValue is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + neId := services.GetUriParamString(r, "ne_id", ",", false, false) + + // no, _ := strconv.ParseInt(neId, 10, 64) + neInfo, err := dborm.XormGetNeInfo(neType, neId) + if err != nil { + log.Error("dborm.XormGetNeInfo is failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + + requestURI2NF := fmt.Sprintf("http://%s:%v%s", neInfo.Ip, neInfo.Port, r.RequestURI) + log.Debug("requestURI2NF: DELETE ", requestURI2NF) + + client := resty.New() + response, err := client.R(). + EnableTrace(). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + Delete(requestURI2NF) + if err != nil { + log.Error("Failed to delete parameter:", err) + services.ResponseInternalServerError500NFConnectRefused(w) + return + } + + services.ResponseWithJson(w, http.StatusNoContent, response) +} diff --git a/features/cm/software.go b/features/cm/software.go new file mode 100644 index 0000000..d65ff53 --- /dev/null +++ b/features/cm/software.go @@ -0,0 +1,911 @@ +package cm + +import ( + "bytes" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "strconv" + "strings" + + "ems.agt/lib/dborm" + "ems.agt/lib/global" + "ems.agt/lib/log" + "ems.agt/lib/services" + "ems.agt/restagent/config" + + "github.com/gorilla/mux" +) + +const ( + SoftwareStatusUploaded = "Uploaded" + SoftwareStatusInactive = "Inactive" + SoftwareStatusActive = "Active" + DigestsSignOkString = "digests signatures OK" + SoftwareVerifiedOk = "Verified OK" +) + +var ( + UriSoftware = config.DefaultUriPrefix + "/systemManagement/{apiVersion}/{neType}/software/{version}" + UriSoftwareNE = config.DefaultUriPrefix + "/systemManagement/{apiVersion}/{neType}/software/{version}/{neId}" + + CustomUriSoftware = config.UriPrefix + "/systemManagement/{apiVersion}/{neType}/software/{version}" + CustomUriSoftwareNE = config.UriPrefix + "/systemManagement/{apiVersion}/{neType}/software/{version}/{neId}" +) + +// 验证签名 +func verify_signature(public_key_name string, source_cms_file string, source_file string) bytes.Buffer { + cmd := exec.Command("/usr/local/omc/run/iv", "verify_signature", public_key_name, source_cms_file, source_file) + var out bytes.Buffer + cmd.Stdout = &out + cmd.Env = append(os.Environ(), + "FOO=duplicate_value", // 重复被忽略 + "FOO=actual_value", // 实际被使用 + ) + err := cmd.Run() + if err != nil { + log.Error(err) + } + + return out +} + +func UploadSoftwareFile(w http.ResponseWriter, r *http.Request) { + log.Debug("UploadSoftwareFile processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Http request error:", err) + return + } + + vars := mux.Vars(r) + neType := vars["neType"] + if neType == "" { + log.Error("neType is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + neTypeUpper := strings.ToUpper(neType) + neTypeLower := strings.ToLower(neType) + + version := vars["version"] + if version == "" { + log.Error("version is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + md5Param := services.GetUriParamString(r, "md5Sum", ",", false, false) + + // body, err := io.ReadAll(io.LimitReader(r.Body, global.RequestBodyMaxLen)) + // if err != nil { + // log.Error("io.ReadAll is failed:", err) + // services.ResponseNotFound404UriNotExist(w, r) + // return + // } + // neSWBody := new(dborm.NeSoftware) + // _ = json.Unmarshal(body, neSWBody) + // log.Trace("neSoftware:", neSWBody) + + softwarePath := fmt.Sprintf("%s/%s", config.GetYamlConfig().OMC.Software, neTypeLower) + fileName, err := services.HandleUploadFile(r, softwarePath, "") + if err != nil { + log.Error("Faile to HandleUploadFile:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + filePrefix := fileName[:strings.Index(fileName, ".zip")] + filePath := fmt.Sprintf("%s/%s", softwarePath, fileName) + log.Debugf("filePath:%s filePrefix:%s softwarePath:%s", filePath, filePrefix, softwarePath) + cmd := exec.Command("unzip", "-o", filePath) + cmd.Dir = softwarePath + out, err := cmd.CombinedOutput() + log.Debugf("Exec outpout:%s", string(out)) + if err != nil { + log.Error("Failed to unzip:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + md5File, err := global.GetFileMD5Sum(filePath) + if err != nil { + log.Error("Faile to GetFileMD5Sum:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + if md5File != md5Param { + err = global.ErrCMNotMatchMD5File + log.Error(err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + rpmFileName := filePrefix + ".rpm" + if config.GetYamlConfig().OMC.CheckSign { + rpmFilePath := softwarePath + "/" + rpmFileName + cmsFileName := rpmFileName + ".cms" + cmsFilePath := softwarePath + "/" + cmsFileName + log.Debugf("cmsFilePath:%s rpmFilePath:%s publicKey:%s", rpmFilePath, cmsFilePath, config.GetYamlConfig().Auth.PublicKey) + result := verify_signature(config.GetYamlConfig().Auth.PublicKey, cmsFilePath, rpmFilePath) + log.Debug("result:", result.String()) + if !strings.Contains(result.String(), SoftwareVerifiedOk) { + err = global.ErrCMNotMatchSignFile + log.Error(err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + // cmd := exec.Command("rpm", "-K", filePath) + // out, err := cmd.CombinedOutput() + // log.Debugf("Exec outpout:%s", string(out)) + // if err != nil { + // log.Error("Failed to execute rpm:", err) + // services.ResponseInternalServerError500ProcessError(w, err) + // return + // } + // if !strings.Contains(string(out), DigestsSignOkString) { + // err = global.ErrCMNotMatchSignFile + // log.Error(err) + // services.ResponseInternalServerError500ProcessError(w, err) + // return + // } + } + + //neBackup := dborm.NeBackup{NeType: neType, NeId: neId, Md5Sum: md5Sum} + //log.Debug("neBackup:", neBackup) + where := fmt.Sprintf("ne_type='%s' and version='%s'", neTypeUpper, version) + has, err := dborm.XormExistTableOne("ne_software", where) + if err != nil { + log.Error("Faile to XormInsertTableOne:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } else if has { + err = global.ErrCMExistSoftwareFile + log.Error(err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + neSoftware := dborm.NeSoftware{ + NeType: neTypeUpper, + FileName: rpmFileName, + Path: softwarePath, + Version: version, + Md5Sum: md5Param, + Comment: neType + " 5GC " + version, + //Comment: neSWBody.Comment, + } + + _, err = dborm.XormInsertTableOne("ne_software", neSoftware) + if err != nil { + log.Error("Faile to XormInsertTableOne:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + services.ResponseStatusOK204NoContent(w) +} + +func UploadSoftwareMultiFile(w http.ResponseWriter, r *http.Request) { + log.Info("UploadSoftwareMultiFile processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Http request error:", err) + return + } + + vars := mux.Vars(r) + neType := vars["neType"] + if neType == "" { + log.Error("neType is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + neTypeUpper := strings.ToUpper(neType) + neTypeLower := strings.ToLower(neType) + + version := vars["version"] + if version == "" { + log.Error("version is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + md5Param := services.GetUriParamString(r, "md5Sum", ",", false, false) + + softwarePath := fmt.Sprintf("%s/%s", config.GetYamlConfig().OMC.Software, neTypeLower) + err = os.MkdirAll(softwarePath, os.ModePerm) + if err != nil { + log.Error("Failed to Mkdir:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + //fileName, err := services.HandleUploadFile(r, softwarePath, "") + + // 解析multipart/form-data请求 + err = r.ParseMultipartForm(200 << 20) // 200MB + if err != nil { + log.Error("Faile to ParseMultipartForm:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + // 获取文件和数据 + swFile := r.MultipartForm.File["file"] + cmsFile := r.MultipartForm.File["cms"] + data := r.MultipartForm.Value["comment"] + + var softwareFileName, cmsFileName, comment string + + // 处理软件rpm/deb文件 + if len(swFile) > 0 { + file := swFile[0] + // 打开文件 + f, err := file.Open() + if err != nil { + log.Error("Faile to Open:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + defer f.Close() + + // 创建本地文件 + dst, err := os.Create(softwarePath + "/" + file.Filename) + if err != nil { + log.Error("Faile to Create:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + defer dst.Close() + + softwareFileName = file.Filename + // 将文件内容拷贝到本地文件 + _, err = io.Copy(dst, f) + if err != nil { + log.Error("Faile to Copy:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + } + + // 处理cms文件 + if len(cmsFile) > 0 { + file := cmsFile[0] + // 打开文件 + f, err := file.Open() + if err != nil { + log.Error("Faile to Open:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + defer f.Close() + + // 创建本地文件 + dst, err := os.Create(softwarePath + "/" + file.Filename) + if err != nil { + log.Error("Faile to Create:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + defer dst.Close() + + cmsFileName = file.Filename + // 将文件内容拷贝到本地文件 + _, err = io.Copy(dst, f) + if err != nil { + log.Error("Faile to Copy:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + } + + // 处理数据 + if len(data) > 0 { + comment = data[0] + } + + if config.GetYamlConfig().OMC.CheckSign && cmsFileName != "" { + rpmFilePath := softwarePath + "/" + softwareFileName + cmsFileName := cmsFileName + cmsFilePath := softwarePath + "/" + cmsFileName + log.Debugf("cmsFilePath:%s rpmFilePath:%s publicKey:%s", rpmFilePath, cmsFilePath, config.GetYamlConfig().Auth.PublicKey) + result := verify_signature(config.GetYamlConfig().Auth.PublicKey, cmsFilePath, rpmFilePath) + log.Debug("result:", result.String()) + if !strings.Contains(result.String(), SoftwareVerifiedOk) { + err = global.ErrCMNotMatchSignFile + log.Error(err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + // cmd := exec.Command("rpm", "-K", filePath) + // out, err := cmd.CombinedOutput() + // log.Debugf("Exec outpout:%s", string(out)) + // if err != nil { + // log.Error("Failed to execute rpm:", err) + // services.ResponseInternalServerError500ProcessError(w, err) + // return + // } + // if !strings.Contains(string(out), DigestsSignOkString) { + // err = global.ErrCMNotMatchSignFile + // log.Error(err) + // services.ResponseInternalServerError500ProcessError(w, err) + // return + // } + } + + // //neBackup := dborm.NeBackup{NeType: neType, NeId: neId, Md5Sum: md5Sum} + // //log.Debug("neBackup:", neBackup) + // where := fmt.Sprintf("ne_type='%s' and version='%s'", neTypeUpper, version) + // has, err := dborm.XormExistTableOne("ne_software", where) + // if err != nil { + // log.Error("Faile to XormInsertTableOne:", err) + // services.ResponseInternalServerError500ProcessError(w, err) + // return + // } else if has == true { + // err = global.ErrCMExistSoftwareFile + // log.Error(err) + // services.ResponseInternalServerError500ProcessError(w, err) + // return + // } + + neSoftware := dborm.NeSoftware{ + NeType: neTypeUpper, + FileName: softwareFileName, + Path: softwarePath, + Version: version, + Md5Sum: md5Param, + Comment: comment, + } + + _, err = dborm.XormInsertTableOne("ne_software", neSoftware) + if err != nil { + log.Error("Faile to XormInsertTableOne:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + services.ResponseStatusOK204NoContent(w) +} + +func DownloadSoftwareFile(w http.ResponseWriter, r *http.Request) { + log.Debug("DownloadSoftwareFile processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + vars := mux.Vars(r) + neType := vars["neType"] + if neType == "" { + log.Error("neType is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + neTypeUpper := strings.ToUpper(neType) + //neTypeLower := strings.ToLower(neType) + + version := vars["version"] + if version == "" { + log.Error("version is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + + sql := fmt.Sprintf("select * from ne_software where ne_type='%s' and version='%s'", neTypeUpper, version) + neSoftware, err := dborm.XormGetDataBySQL(sql) + if err != nil { + log.Error("Faile to XormGetDataBySQL:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } else if len(*neSoftware) == 0 { + err := global.ErrCMNotFoundTargetSoftware + log.Error(err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + fileName := (*neSoftware)[0]["file_name"] + path := (*neSoftware)[0]["path"] + md5Sum := (*neSoftware)[0]["md5_sum"] + + services.ResponseFileWithNameAndMD5(w, http.StatusOK, fileName, path, md5Sum) +} + +func DeleteSoftwareFile(w http.ResponseWriter, r *http.Request) { + log.Debug("DeleteSoftwareFile processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + vars := mux.Vars(r) + neType := vars["neType"] + if neType == "" { + log.Error("neType is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + neTypeUpper := strings.ToUpper(neType) + //neTypeLower := strings.ToLower(neType) + + version := vars["version"] + if version == "" { + log.Error("version is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + + sql := fmt.Sprintf("select * from ne_software where ne_type='%s' and version='%s'", neTypeUpper, version) + neSoftware, err := dborm.XormGetDataBySQL(sql) + if err != nil { + log.Error("Faile to XormGetDataBySQL:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } else if len(*neSoftware) == 0 { + err := global.ErrCMNotFoundTargetSoftware + log.Error(err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + where := fmt.Sprintf("ne_type='%s' and version='%s'", neTypeUpper, version) + affected, err := dborm.XormDeleteDataByWhere(where, "ne_software") + if err != nil || affected == 0 { + log.Error("Faile to XormGetDataBySQL:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + fileName := (*neSoftware)[0]["file_name"] + path := (*neSoftware)[0]["path"] + filePath := fmt.Sprintf("%s/%s", path, fileName) + err = os.Remove(filePath) + if err != nil { + log.Error("Faile to Remove:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + services.ResponseStatusOK204NoContent(w) +} + +func DistributeSoftwareToNF(w http.ResponseWriter, r *http.Request) { + log.Debug("DistributeSoftwareFile processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Http request error:", err) + return + } + + vars := mux.Vars(r) + neType := vars["neType"] + if neType == "" { + log.Error("neType is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + neTypeUpper := strings.ToUpper(neType) + neTypeLower := strings.ToLower(neType) + + version := vars["version"] + if version == "" { + log.Error("version is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + + neId := vars["neId"] + if version == "" { + log.Error("neId is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + + neInfo, err := dborm.XormGetNeInfo(neTypeUpper, neId) + if err != nil { + log.Error("dborm.XormGetNeInfo is failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + + sql := fmt.Sprintf("select * from ne_software where ne_type='%s' and version='%s'", neTypeUpper, version) + neSoftware, err := dborm.XormGetDataBySQL(sql) + if err != nil { + log.Error("Faile to XormGetDataBySQL:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } else if len(*neSoftware) == 0 { + err := global.ErrCMNotFoundTargetSoftware + log.Error(err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + log.Debug("neSoftware:", neSoftware) + + sql = fmt.Sprintf("select * from ne_version where ne_type='%s' and ne_id='%s'", neTypeUpper, neId) + neVersion, err := dborm.XormGetDataBySQL(sql) + if err != nil { + log.Error("Faile to XormGetDataBySQL:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + log.Debug("neVersion:", neVersion) + + sshHost := fmt.Sprintf("%s@%s", config.GetYamlConfig().NE.User, neInfo.Ip) + mkdirCmd := fmt.Sprintf("sudo mkdir -p %s/software/%s", config.GetYamlConfig().NE.OmcDir, neTypeLower) + cmd := exec.Command("ssh", sshHost, mkdirCmd) + out, err := cmd.CombinedOutput() + log.Debugf("Exec output: %v", string(out)) + if err != nil { + log.Error("Faile to mkdir:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + fileName := (*neSoftware)[0]["file_name"] + path := (*neSoftware)[0]["path"] + srcFile := fmt.Sprintf("%s/%s", path, fileName) + + scpDir := fmt.Sprintf("%s@%s:%s", config.GetYamlConfig().NE.User, + neInfo.Ip, config.GetYamlConfig().NE.ScpDir) + cmd = exec.Command("scp", "-r", srcFile, scpDir) + out, err = cmd.CombinedOutput() + log.Debugf("Exec output: %v", string(out)) + if err != nil { + log.Errorf("Faile to scp NF: neType=%s, neId=%s, ip=%s", neType, neId, neInfo.Ip) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + neFilePath := config.GetYamlConfig().NE.ScpDir + "/" + fileName + cpCmd := fmt.Sprintf("sudo cp -f %s %s/software/%s", neFilePath, + config.GetYamlConfig().NE.OmcDir, neTypeLower) + cmd = exec.Command("ssh", sshHost, cpCmd) + out, err = cmd.CombinedOutput() + log.Debugf("Exec output: %v", string(out)) + if err != nil { + log.Error("Faile to execute cp command:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + if len(*neVersion) == 0 { + neVersionData := dborm.NeVersion{ + NeType: neTypeUpper, + NeId: neInfo.NeId, + Version: (*neSoftware)[0]["version"], + FilePath: fmt.Sprintf("%s/software/%s/%s", config.GetYamlConfig().NE.OmcDir, neTypeLower, fileName), + PreVersion: "", + PreFile: "", + Status: SoftwareStatusInactive, + } + + _, err = dborm.XormInsertTableOne("ne_version", neVersionData) + if err != nil { + log.Error("Faile to XormInsertTableOne:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + } else { + idNeVersion, _ := strconv.Atoi((*neVersion)[0]["id"]) + neVersionData := dborm.NeVersion{ + NeType: neTypeUpper, + NeId: neInfo.NeId, + Version: (*neSoftware)[0]["version"], + FilePath: fmt.Sprintf("%s/software/%s/%s", config.GetYamlConfig().NE.OmcDir, neTypeLower, fileName), + PreVersion: (*neVersion)[0]["version"], + PreFile: (*neVersion)[0]["file_path"], + Status: SoftwareStatusInactive, + } + + _, err = dborm.XormUpdateTableById(idNeVersion, "ne_version", neVersionData) + if err != nil { + log.Error("Faile to UpdateTableById:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + } + + services.ResponseStatusOK204NoContent(w) +} + +func ActiveSoftwareToNF(w http.ResponseWriter, r *http.Request) { + log.Debug("ActiveSoftwareToNF processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Http request error:", err) + return + } + + vars := mux.Vars(r) + neType := vars["neType"] + if neType == "" { + log.Error("neType is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + neTypeUpper := strings.ToUpper(neType) + neTypeLower := strings.ToLower(neType) + + version := vars["version"] + if version == "" { + log.Error("version is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + + neId := vars["neId"] + if version == "" { + log.Error("neId is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + + neInfo, err := dborm.XormGetNeInfo(neTypeUpper, neId) + if err != nil { + log.Error("dborm.XormGetNeInfo is failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + + sql := fmt.Sprintf("select * from ne_software where ne_type='%s' and version='%s'", neTypeUpper, version) + neSoftware, err := dborm.XormGetDataBySQL(sql) + if err != nil { + log.Error("Faile to XormGetDataBySQL:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } else if len(*neSoftware) == 0 { + err := global.ErrCMNotFoundTargetSoftware + log.Error(err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + log.Debug("neVersion:", neSoftware) + + sql = fmt.Sprintf("select * from ne_version where ne_type='%s' and ne_id='%s' and version='%s'", neTypeUpper, neId, version) + neVersion, err := dborm.XormGetDataBySQL(sql) + if err != nil { + log.Error("Faile to XormGetDataBySQL:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } else if len(*neVersion) == 0 { + err := global.ErrCMNotFoundTargetNeVersion + log.Error(err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + log.Debug("neVersion:", neVersion) + + if !config.GetYamlConfig().OMC.TestMode { + filePath := (*neVersion)[0]["file_path"] + sshHost := fmt.Sprintf("%s@%s", config.GetYamlConfig().NE.User, neInfo.Ip) + fileType := global.IsRpmOrDebPackage(filePath) + if fileType == 1 { + rpmCmd := fmt.Sprintf("sudo rpm -Uvh '%s'", filePath) + cmd := exec.Command("ssh", sshHost, rpmCmd) + out, err := cmd.CombinedOutput() + log.Debugf("Exec output: %v", string(out)) + if err != nil { + log.Error("Faile to execute rpm command:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + } else if fileType == 2 { + dpkgCmd := fmt.Sprintf("sudo dpkg -i '%s'", filePath) + cmd := exec.Command("ssh", sshHost, dpkgCmd) + out, err := cmd.CombinedOutput() + log.Debugf("Exec output: %v", string(out)) + if err != nil { + log.Error("Faile to execute dpkg command:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + } else { + err := global.ErrCMUnknownSoftwareFormat + log.Error(err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + switch neTypeLower { + case "omc": + restartCmd := fmt.Sprintf("sudo %s/bin/omcsvc.sh restart", config.GetYamlConfig().NE.OmcDir) + cmd := exec.Command("ssh", sshHost, restartCmd) + out, err := cmd.CombinedOutput() + log.Debugf("Exec output: %v", string(out)) + if err != nil { + log.Error("Faile to execute ssh restart omc:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + case "ims": + restartCmd := "sudo ims-stop && sudo ims-start" + cmd := exec.Command("ssh", sshHost, restartCmd) + out, err := cmd.CombinedOutput() + log.Debugf("Exec output: %v", string(out)) + if err != nil { + log.Error("Faile to execute ssh sudo systemctl command:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + default: + restartCmd := fmt.Sprintf("sudo systemctl restart %s.service", neTypeLower) + cmd := exec.Command("ssh", sshHost, restartCmd) + out, err := cmd.CombinedOutput() + log.Debugf("Exec output: %v", string(out)) + if err != nil { + log.Error("Faile to execute ssh sudo systemctl command:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + } + } + + idNeVersion, _ := strconv.Atoi((*neVersion)[0]["id"]) + neVersionData := dborm.NeVersion{ + Status: SoftwareStatusActive, + } + + _, err = dborm.XormUpdateTableById(idNeVersion, "ne_version", neVersionData) + if err != nil { + log.Error("Faile to UpdateTableById:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + services.ResponseStatusOK204NoContent(w) +} + +func RollBackSoftwareToNF(w http.ResponseWriter, r *http.Request) { + log.Debug("ActiveSoftwareToNF processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Http request error:", err) + return + } + + vars := mux.Vars(r) + neType := vars["neType"] + if neType == "" { + log.Error("neType is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + neTypeUpper := strings.ToUpper(neType) + neTypeLower := strings.ToLower(neType) + + version := vars["version"] + if version == "" { + log.Error("version is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + + neId := vars["neId"] + if version == "" { + log.Error("neId is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + + neInfo, err := dborm.XormGetNeInfo(neTypeUpper, neId) + if err != nil { + log.Error("dborm.XormGetNeInfo is failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + + sql := fmt.Sprintf("select * from ne_version where ne_type='%s' and ne_id='%s'", neTypeUpper, neId) + neVersion, err := dborm.XormGetDataBySQL(sql) + if err != nil { + log.Error("Faile to XormGetDataBySQL:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } else if len(*neVersion) == 0 { + err := global.ErrCMNotFoundTargetNeVersion + log.Error(err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + log.Debug("neVersion:", neVersion) + + filePath := (*neVersion)[0]["pre_file"] + if filePath == "" { + err := global.ErrCMNotFoundRollbackNeVersion + log.Error(err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + if !config.GetYamlConfig().OMC.TestMode { + sshHost := fmt.Sprintf("%s@%s", config.GetYamlConfig().NE.User, neInfo.Ip) + fileType := global.IsRpmOrDebPackage(filePath) + if fileType == 1 { + rpmCmd := fmt.Sprintf("sudo rpm -Uvh --oldpackage '%s'", filePath) + cmd := exec.Command("ssh", sshHost, rpmCmd) + out, err := cmd.CombinedOutput() + log.Debugf("Exec output: %v", string(out)) + if err != nil { + log.Error("Faile to execute rpm command:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + } else if fileType == 2 { + dpkgCmd := fmt.Sprintf("sudo dpkg -i '%s'", filePath) + cmd := exec.Command("ssh", sshHost, dpkgCmd) + out, err := cmd.CombinedOutput() + log.Debugf("Exec output: %v", string(out)) + if err != nil { + log.Error("Faile to execute dpkg command:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + } else { + err := global.ErrCMUnknownSoftwareFormat + log.Error(err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + switch neTypeLower { + case "omc": + restartCmd := fmt.Sprintf("sudo %s/bin/omcsvc.sh restart", config.GetYamlConfig().NE.OmcDir) + cmd := exec.Command("ssh", sshHost, restartCmd) + out, err := cmd.CombinedOutput() + log.Debugf("Exec output: %v", string(out)) + if err != nil { + log.Error("Faile to execute ssh restart omc:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + case "ims": + restartCmd := "sudo ims-stop && sudo ims-start" + cmd := exec.Command("ssh", sshHost, restartCmd) + out, err := cmd.CombinedOutput() + log.Debugf("Exec output: %v", string(out)) + if err != nil { + log.Error("Faile to execute ssh sudo systemctl command:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + default: + restartCmd := fmt.Sprintf("sudo systemctl restart %s.service", neTypeLower) + cmd := exec.Command("ssh", sshHost, restartCmd) + out, err := cmd.CombinedOutput() + log.Debugf("Exec output: %v", string(out)) + if err != nil { + log.Error("Faile to execute ssh sudo systemctl command:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + } + } + + idNeVersion, _ := strconv.Atoi((*neVersion)[0]["id"]) + neVersionData := dborm.NeVersion{ + Version: (*neVersion)[0]["pre_version"], + FilePath: (*neVersion)[0]["pre_file"], + PreVersion: "-", + PreFile: "-", + NewVersion: (*neVersion)[0]["version"], + NewFile: (*neVersion)[0]["file_path"], + Status: SoftwareStatusActive, + } + + _, err = dborm.XormUpdateTableById(idNeVersion, "ne_version", neVersionData) + if err != nil { + log.Error("Faile to UpdateTableById:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + services.ResponseStatusOK204NoContent(w) +} diff --git a/features/dbrest/backup.go b/features/dbrest/backup.go new file mode 100644 index 0000000..f8cfb05 --- /dev/null +++ b/features/dbrest/backup.go @@ -0,0 +1,134 @@ +package dbrest + +import ( + "archive/tar" + "compress/gzip" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "time" + + "ems.agt/lib/core/conf" + "ems.agt/lib/dborm" + "ems.agt/lib/log" + "ems.agt/lib/services" + "ems.agt/restagent/config" + _ "github.com/go-sql-driver/mysql" +) + +var ( + // 系统备份-数据库备份 + UriDbBackup = config.DefaultUriPrefix + "/dataManagement/{apiVersion}/dbBackup" + CustomUriDbBackup = config.UriPrefix + "/dataManagement/{apiVersion}/dbBackup" // for external + + // 系统备份-文件备份 + UriConfBackup = config.DefaultUriPrefix + "/dataManagement/{apiVersion}/confBackup" + CustomUriConfBackup = config.UriPrefix + "/dataManagement/{apiVersion}/confBackup" // for external + +) + +// DbBackup 系统备份-数据库备份1 +func DbBackup(w http.ResponseWriter, r *http.Request) { + dbConfigHost := conf.Get("database.host").(string) + dbConfigUser := conf.Get("database.user").(string) + dbConfigPassword := conf.Get("database.password").(string) + dbConfigBackup := conf.Get("database.backup").(string) + + // 备份SQL文件路径 + fileName := "database_backup_" + time.Now().Format("20060102150405") + ".sql" + backupFile := dbConfigBackup + "/" + fileName + + // 执行mysqldump命令进行备份 + cmd := exec.Command("mysqldump", "-u", dbConfigUser, "-p"+dbConfigPassword, "-h", dbConfigHost, "--all-databases", ">", backupFile) + err := cmd.Start() + if err != nil { + services.ResponseErrorWithJson(w, 400, err.Error()) + return + } + + // 备份结果 + res, err := dborm.DbClient.XEngine.Exec("INSERT INTO `sys_backup`(`id`, `created_at`, `updated_at`, `backup_type`, `backup_way`, `name`, `path`) VALUES (null, ?, ?, '1', '0', ?, ?);", time.Now(), time.Now(), fileName, backupFile) + if err != nil { + services.ResponseErrorWithJson(w, 500, err.Error()) + return + } + services.ResponseWithJson(w, 200, res) + log.Info("Backup completed successfully.") +} + +// 系统备份-文件备份0 +func ConfBackup(w http.ResponseWriter, r *http.Request) { + dbConfigBackup := conf.Get("database.backup").(string) + etcDir := conf.Get("ne.omcdir").(string) + + // 文件名 + fileName := "conf_backup_" + time.Now().Format("20060102150405") + ".tar.gz" + backupFile := dbConfigBackup + "/" + fileName + + err := createTarGz(etcDir, backupFile) + if err != nil { + services.ResponseErrorWithJson(w, 500, err.Error()) + return + } + + // 备份结果 + res, err := dborm.DbClient.XEngine.Exec("INSERT INTO `sys_backup`(`id`, `created_at`, `updated_at`, `backup_type`, `backup_way`, `name`, `path`) VALUES (null, ?, ?, '1', '0', ?, ?);", time.Now(), time.Now(), fileName, backupFile) + if err != nil { + services.ResponseErrorWithJson(w, 500, err.Error()) + return + } + services.ResponseWithJson(w, 200, res) + log.Info("Backup completed successfully.") +} + +// 打压缩 +func createTarGz(source, target string) error { + tarFile, err := os.Create(target) + if err != nil { + return err + } + defer tarFile.Close() + + gw := gzip.NewWriter(tarFile) + defer gw.Close() + + tw := tar.NewWriter(gw) + defer tw.Close() + + return filepath.Walk(source, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + header, err := tar.FileInfoHeader(info, "") + if err != nil { + return err + } + + relPath, err := filepath.Rel(source, path) + if err != nil { + return err + } + + header.Name = relPath + + if err := tw.WriteHeader(header); err != nil { + return err + } + + if !info.Mode().IsRegular() { + return nil + } + + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + _, err = io.Copy(tw, file) + return err + }) +} diff --git a/features/dbrest/dbrest.go b/features/dbrest/dbrest.go new file mode 100644 index 0000000..a529794 --- /dev/null +++ b/features/dbrest/dbrest.go @@ -0,0 +1,916 @@ +package dbrest + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "regexp" + "strings" + "time" + + "ems.agt/lib/core/utils/ctx" + "ems.agt/lib/dborm" + "ems.agt/lib/global" + "ems.agt/lib/log" + "ems.agt/lib/services" + "ems.agt/restagent/config" + + "github.com/gorilla/mux" + "xorm.io/xorm" +) + +type XormResponse struct { + Data interface{} `json:"data"` +} + +type XormInsertResponse struct { + Data interface{} `json:"data"` +} + +var ( + // database management rest pattern, discard + XormGetDataUri = config.DefaultUriPrefix + "/databaseManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/{objectTypeValue}" + XormSelectDataUri = config.DefaultUriPrefix + "/databaseManagement/{apiVersion}/select/{elementTypeValue}/{objectTypeValue}" + XormInsertDataUri = config.DefaultUriPrefix + "/databaseManagement/{apiVersion}/insert/{elementTypeValue}/{objectTypeValue}" + XormUpdateDataUri = config.DefaultUriPrefix + "/databaseManagement/{apiVersion}/update/{elementTypeValue}/{objectTypeValue}" + XormDeleteDataUri = config.DefaultUriPrefix + "/databaseManagement/{apiVersion}/delete/{elementTypeValue}/{objectTypeValue}" + + CustomXormGetDataUri = config.UriPrefix + "/databaseManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/{objectTypeValue}" + CustomXormSelectDataUri = config.UriPrefix + "/databaseManagement/{apiVersion}/select/{elementTypeValue}/{objectTypeValue}" + CustomXormInsertDataUri = config.UriPrefix + "/databaseManagement/{apiVersion}/insert/{elementTypeValue}/{objectTypeValue}" + CustomXormUpdateDataUri = config.UriPrefix + "/databaseManagement/{apiVersion}/update/{elementTypeValue}/{objectTypeValue}" + CustomXormDeleteDataUri = config.UriPrefix + "/databaseManagement/{apiVersion}/delete/{elementTypeValue}/{objectTypeValue}" + + XormCommonUri = config.DefaultUriPrefix + "/databaseManagement/{apiVersion}/{elementTypeValue}/{objectTypeValue}" // for internal + XormDatabaseUri = config.DefaultUriPrefix + "/database/{apiVersion}/{elementTypeValue}/{objectTypeValue}" // for crontask + XormExtDataUri = config.DefaultUriPrefix + "/dataManagement/{apiVersion}/{elementTypeValue}/{objectTypeValue}" // for external + XormDataSQLUri = config.DefaultUriPrefix + "/dataManagement/{apiVersion}/{elementTypeValue}/{objectTypeValue}" // for external + + CustomXormCommonUri = config.UriPrefix + "/databaseManagement/{apiVersion}/{elementTypeValue}/{objectTypeValue}" // for internal + CustomXormExtDataUri = config.UriPrefix + "/dataManagement/{apiVersion}/{elementTypeValue}/{objectTypeValue}" // for external + CustomXormDataSQLUri = config.UriPrefix + "/dataManagement/{apiVersion}/{elementTypeValue}/{objectTypeValue}" // for external + + // 查询数据库连接情况 + UriDbConnection = config.DefaultUriPrefix + "/dataManagement/{apiVersion}/dbConnection" + CustomUriDbConnection = config.UriPrefix + "/dataManagement/{apiVersion}/dbConnection" // for external + + // 终结非法的数据库连接 + UriDbStop = config.DefaultUriPrefix + "/dataManagement/{apiVersion}/dbStop" + CustomUriDbStop = config.UriPrefix + "/dataManagement/{apiVersion}/dbStop" // for external + +) + +var xormResponse XormResponse + +var XEngine *xorm.Engine + +type DatabaseClient struct { + dbType string + dbUrl string + dbConnMaxLifetime time.Duration + dbMaxIdleConns int + dbMaxOpenConns int + IsShowSQL bool + + XEngine *xorm.Engine +} + +var DbClient DatabaseClient + +func InitDbClient(dbType, dbUser, dbPassword, dbHost, dbPort, dbName string) error { + DbClient.dbUrl = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=true&loc=Local", + dbUser, dbPassword, dbHost, dbPort, dbName) + DbClient.dbType = dbType + DbClient.dbConnMaxLifetime = 0 + DbClient.dbMaxIdleConns = 0 + DbClient.dbMaxOpenConns = 0 + if log.GetLevel() == log.LOG_TRACE { + DbClient.IsShowSQL = true + } + log.Debugf("dbType:%s dbUrl:%s:******@tcp(%s:%s)/%s??charset=utf8&parseTime=true&loc=Local", + dbType, dbUser, dbHost, dbPort, dbName) + + var err error + DbClient.XEngine, err = xorm.NewEngine(DbClient.dbType, DbClient.dbUrl) + if err != nil { + log.Error("Failed to connet database:", err) + return err + } + DbClient.XEngine.SetConnMaxLifetime(DbClient.dbConnMaxLifetime) + DbClient.XEngine.SetMaxIdleConns(DbClient.dbMaxIdleConns) + DbClient.XEngine.SetMaxOpenConns(DbClient.dbMaxOpenConns) + if DbClient.IsShowSQL { + DbClient.XEngine.ShowSQL(true) + } + XEngine = DbClient.XEngine + + return nil +} + +func GetUriSQLArray(r *http.Request) []string { + var sa []string + vars := r.URL.Query() + + // 默认SQL + if s, ok := vars["SQL"]; ok { + for _, r := range s { + if r != "" { + sa = append(sa, r) + } + } + } + + // 查询总数 + if totalSQL, ok := vars["totalSQL"]; ok { + if totalSQL[0] != "" { + sa = append(sa, totalSQL[0]) + } + } + // 查询列表 + if rowsSQL, ok := vars["rowsSQL"]; ok { + if rowsSQL[0] != "" { + sa = append(sa, rowsSQL[0]) + } + } + + if len(sa) == 0 { + log.Info("SQL is not exist") + return nil + } + + log.Debug("SQL array:", sa) + return sa +} + +// Get table name from SQL +func GetTableNameFromSQL(s string) string { + ls := strings.ToLower(s) + i1 := strings.Index(ls, "from") + i2 := strings.Index(ls, "where") + + var ts string + if i1 > 0 { + if i2 > 0 && i2 > i1 { + ts = ls[i1+4 : i2] + } + if i2 < 0 { + ts = ls[i1+4:] + } + } + + tn := strings.Trim(ts, " ") + log.Debug("i1:", i1, "i2:", i2, "tn:", tn) + return tn +} + +func GetTableName(sql string) string { + ls := strings.ToLower(sql) + + re := regexp.MustCompile(`from\s+(\S+)`) + matches := re.FindStringSubmatch(ls) + if len(matches) < 2 { + return "" + } + return matches[1] +} + +func IsQuerySQL(s string) bool { + ts := strings.Trim(strings.ToLower(s), " ") + if strings.Index(ts, "select") != 0 { + return false + } + return true +} + +// xorm Get data from database +func ExtDatabaseExecSQL(w http.ResponseWriter, r *http.Request) { + log.Debug("ExtDatabaseExecSQL processing... ") + + var sql []string + var err error + + _, err = services.CheckExtValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + //vars := mux.Vars(r) + //tblName := vars["objectTypeValue"] + sql = GetUriSQLArray(r) + // select as must, todo ... + + ls := services.ExtGetUriPageLimitString(r) + + // data := make([]map[string]interface{}, 0) + // xormResponse := make([]map[string]interface{}, len(sql)) + var xormResponse XormResponse + data := make([]map[string]interface{}, 0) + for i, s := range sql { + log.Tracef("SQL[%d]: %s", i, sql[i]) + + //rows := make([]map[string]interface{}, 0) + mapRows := make(map[string]interface{}) + + if s != "" { + // err = XEngine.SQL(s).Find(&rows) + // if IsQuerySQL(s) == false { + // services.ResponseNotAcceptable406QuerySQLError(w) + // return + // } + + querySQL := s + if i == (len(sql) - 1) { + querySQL = querySQL + " " + ls + } + log.Debug("querySQL:", querySQL) + rows, err := DbClient.XEngine.Exec(querySQL) + if err != nil { + log.Error("SQL failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + tableName := GetTableName(s) + log.Debugf("s:%s tableName:%s", s, tableName) + mapRows[tableName] = rows + data = append(data, mapRows) + log.Trace("data:", data) + } + i++ + } + xormResponse.Data = data + + services.ResponseWithJson(w, http.StatusOK, xormResponse) +} + +// xorm Get data from database +func ExtDatabaseGetData(w http.ResponseWriter, r *http.Request) { + log.Debug("ExtDatabaseGetData processing... ") + + var sql []string + token, err := services.CheckExtValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + pack := "dbrest" + vars := mux.Vars(r) + module := "" + dbname := vars["elementTypeValue"] + tbname := vars["objectTypeValue"] + + log.Debugf("token:%s, method:%s, module:%s, dbname:%s, tbname:%s, pack:%s", token, r.Method, module, dbname, tbname, pack) + + // exist, err := services.CheckUserPermission(token, strings.ToLower(r.Method), module, dbname, tbname, pack) + // if err != nil { + // log.Error("Failed to get permission:", err) + // services.ResponseForbidden403NotPermission(w) + // return + // } + // if !exist { + // log.Error("Not permission!") + // services.ResponseForbidden403NotPermission(w) + // return + // } + + sql = GetUriSQLArray(r) + // select as must, todo ... + if sql == nil { + wc := services.GetUriLocString(r) + if wc == "" { + sql = append(sql, fmt.Sprintf("select * from %s", tbname)) + } else { + sql = append(sql, fmt.Sprintf("select * from %s where %s", tbname, wc)) + } + } + + ls := services.ExtGetUriPageLimitString(r) + + // data := make([]map[string]interface{}, 0) + // xormResponse := make([]map[string]interface{}, len(sql)) + var xormResponse XormResponse + data := make([]map[string]interface{}, 0) + for i, s := range sql { + log.Tracef("SQL[%d]: %s", i, sql[i]) + + rows := make([]map[string]interface{}, 0) + mapRows := make(map[string]interface{}) + + if s != "" { + // err = XEngine.SQL(s).Find(&rows) + if IsQuerySQL(s) == false { + services.ResponseNotAcceptable406QuerySQLError(w) + return + } + + querySQL := s + if i == (len(sql) - 1) { + querySQL = querySQL + " " + ls + } + log.Debug("querySQL:", querySQL) + rows, err = DbClient.XEngine.QueryInterface(querySQL) + if err != nil { + log.Error("SQL failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + tableName := GetTableName(s) + log.Debugf("s:%s tableName:%s", s, tableName) + mapRows[tableName] = rows + data = append(data, mapRows) + log.Trace("data:", data) + } + i++ + } + xormResponse.Data = data + + services.ResponseWithJson(w, http.StatusOK, xormResponse) +} + +func ExtDatabaseInsertData(w http.ResponseWriter, r *http.Request) { + log.Debug("ExtDatabaseInsertData processing... ") + + token, err := services.CheckExtValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + body, err := io.ReadAll(io.LimitReader(r.Body, global.RequestBodyMaxLen)) //io.LimitReader限制大小 + if err != nil { + log.Error("io.ReadAll failed:", err) + services.ResponseNotFound404UriNotExist(w, r) + return + } + + vars := mux.Vars(r) + module := "" + dbname := vars["elementTypeValue"] + tbname := vars["objectTypeValue"] + pack := "dbrest" + + log.Debugf("token:%s, method:%s, module:%s, dbname:%s, tbname:%s, pack:%s", token, r.Method, module, dbname, tbname, pack) + + // exist, err := services.CheckUserPermission(token, strings.ToLower(r.Method), module, dbname, tbname, pack) + // if err != nil { + // log.Error("Failed to get permission:", err) + // services.ResponseForbidden403NotPermission(w) + // return + // } + // if !exist { + // log.Error("permission deny!") + // services.ResponseForbidden403NotPermission(w) + // return + // } + + log.Debug("Request body:", string(body), "dataObject:", tbname) + insertData := make(map[string]interface{}) + _ = json.Unmarshal(body, &insertData) + + tn, sql := dborm.ConstructInsertSQL(tbname, insertData) + log.Tracef("tn: %s sql :%s", tn, sql) + xSession := DbClient.XEngine.NewSession() + defer xSession.Close() + var affected int64 + for _, s := range sql { + res, err := xSession.Exec(s) + if err != nil { + log.Error("Insert failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + n, _ := res.RowsAffected() + affected = affected + n + } + xSession.Commit() + // affected, err := InsertDataWithJson(insertData) + mapRow := make(map[string]interface{}) + row := map[string]interface{}{"affectedRows": affected} + mapRow[tn] = row + // xormResponse.Data = mapRow + services.ResponseWithJson(w, http.StatusOK, mapRow) +} + +func ExtDatabaseUpdateData(w http.ResponseWriter, r *http.Request) { + log.Debug("ExtDatabaseUpdateData processing... ") + + token, err := services.CheckExtValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + vars := mux.Vars(r) + module := "" + dbname := vars["elementTypeValue"] + tbname := vars["objectTypeValue"] + pack := "dbrest" + + log.Debugf("token:%s, method:%s, module:%s, dbname:%s, tbname:%s, pack:%s", token, r.Method, module, dbname, tbname, pack) + + // exist, err := services.CheckUserPermission(token, strings.ToLower(r.Method), module, dbname, tbname, pack) + // if err != nil { + // log.Error("Failed to get permission:", err) + // services.ResponseForbidden403NotPermission(w) + // return + // } + // if !exist { + // log.Error("Not permission!") + // services.ResponseForbidden403NotPermission(w) + // return + // } + + body, err := io.ReadAll(io.LimitReader(r.Body, global.RequestBodyMaxLen)) + if err != nil { + log.Error("io.ReadAll is failed:", err) + services.ResponseNotFound404UriNotExist(w, r) + return + } + + wc := services.GetUriLocString(r) + + log.Debug("Request body:", string(body), "Tablename:", tbname, "wc:", wc) + updateData := make(map[string]interface{}) + _ = json.Unmarshal(body, &updateData) + + tn, sql := dborm.ConstructUpdateSQL(tbname, updateData, wc) + log.Tracef("tn: %s sql :%s", tn, sql) + xSession := DbClient.XEngine.NewSession() + defer xSession.Close() + var affected int64 + for _, s := range sql { + res, err := xSession.Exec(s) + if err != nil { + log.Error("Update failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + n, _ := res.RowsAffected() + affected = affected + n + } + xSession.Commit() + mapRow := make(map[string]interface{}) + row := map[string]interface{}{"affectedRows": affected} + mapRow[tn] = row + services.ResponseWithJson(w, http.StatusOK, mapRow) +} + +func ExtDatabaseDeleteData(w http.ResponseWriter, r *http.Request) { + log.Debug("ExtDatabaseDeleteData processing... ") + + token, err := services.CheckExtValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + vars := mux.Vars(r) + module := "" + dbname := vars["elementTypeValue"] + tbname := vars["objectTypeValue"] + pack := "dbreset" + + log.Debugf("token:%s, method:%s, module:%, dbname:%s, tbname:%s pack:%s", token, r.Method, module, dbname, tbname, pack) + + // exist, err := services.CheckUserPermission(token, strings.ToLower(r.Method), module, dbname, tbname, pack) + // if err != nil { + // log.Error("Failed to get permission:", err) + // services.ResponseForbidden403NotPermission(w) + // return + // } + // if !exist { + // log.Error("Not permission!") + // services.ResponseForbidden403NotPermission(w) + // return + // } + + wc := services.GetUriLocString(r) + + log.Debug("Table name:", tbname, "wc:", wc) + + sql := dborm.ConstructDeleteSQL(tbname, wc) + xSession := DbClient.XEngine.NewSession() + defer xSession.Close() + res, err := xSession.Exec(sql) + if err != nil { + log.Error("Update failed, err:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + affected, _ := res.RowsAffected() + xSession.Commit() + mapRow := make(map[string]interface{}) + row := map[string]interface{}{"affectedRows": affected} + mapRow["data"] = row + services.ResponseWithJson(w, http.StatusOK, mapRow) +} + +// xorm Get data from database +func DatabaseGetData(w http.ResponseWriter, r *http.Request) { + log.Debug("DatabaseGetData processing... ") + + var sql []string + var err error + + _, err = services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + vars := mux.Vars(r) + tblName := vars["objectTypeValue"] + sql = GetUriSQLArray(r) + // select as must, todo ... + + if sql == nil { + wc := services.GetUriWhereString(r) + if wc == "" { + sql = append(sql, fmt.Sprintf("select * from %s", tblName)) + } else { + sql = append(sql, fmt.Sprintf("select * from %s where %s", tblName, wc)) + } + } + + ls := services.GetUriPageLimitString(r) + + // data := make([]map[string]interface{}, 0) + // xormResponse := make([]map[string]interface{}, len(sql)) + var xormResponse XormResponse + data := make([]map[string]interface{}, 0) + for i, s := range sql { + log.Tracef("SQL[%d]: %s", i, sql[i]) + + rows := make([]map[string]interface{}, 0) + mapRows := make(map[string]interface{}) + + if s != "" { + // err = XEngine.SQL(s).Find(&rows) + if IsQuerySQL(s) == false { + services.ResponseNotAcceptable406QuerySQLError(w) + return + } + + querySQL := s + if i == (len(sql) - 1) { + querySQL = querySQL + " " + ls + } + log.Debug("querySQL:", querySQL) + rows, err = DbClient.XEngine.QueryInterface(querySQL) + if err != nil { + log.Error("SQL failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + tableName := GetTableName(s) + log.Debugf("s:%s tableName:%s", s, tableName) + mapRows[tableName] = rows + data = append(data, mapRows) + log.Trace("data:", data) + } + i++ + } + xormResponse.Data = data + + services.ResponseWithJson(w, http.StatusOK, xormResponse) +} + +func DatabaseInsertData(w http.ResponseWriter, r *http.Request) { + log.Debug("DatabaseInsertData processing... ") + + body, err := io.ReadAll(io.LimitReader(r.Body, global.RequestBodyMaxLen)) //io.LimitReader限制大小 + if err != nil { + log.Error("io.ReadAll failed:", err) + services.ResponseNotFound404UriNotExist(w, r) + return + } + + vars := mux.Vars(r) + tableName := vars["objectTypeValue"] + log.Debug("Request body:", string(body), "tableName:", tableName) + insertData := make(map[string]interface{}) + _ = json.Unmarshal(body, &insertData) + + // 操作日志的IP + if tableName == "operation_log" || tableName == "security_log" { + ipAddr := strings.Split(r.RemoteAddr, ":")[0] + s := insertData["data"].([]any) + a := s[0].(map[string]any) + a["op_ip"] = ipAddr + } else if tableName == "mml_log" { + ipAddr := strings.Split(r.RemoteAddr, ":")[0] + s := insertData["data"].([]any) + a := s[0].(map[string]any) + a["ip"] = ipAddr + } + + tn, sql := dborm.ConstructInsertSQL(tableName, insertData) + log.Tracef("tn: %s sql :%s", tn, sql) + xSession := DbClient.XEngine.NewSession() + defer xSession.Close() + var affected int64 + for _, s := range sql { + res, err := xSession.Exec(s) + if err != nil { + log.Error("Insert failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + n, _ := res.RowsAffected() + affected = affected + n + } + xSession.Commit() + // affected, err := InsertDataWithJson(insertData) + mapRow := make(map[string]interface{}) + row := map[string]interface{}{"affectedRows": affected} + mapRow[tn] = row + // xormResponse.Data = mapRow + services.ResponseWithJson(w, http.StatusOK, mapRow) +} + +func DatabaseUpdateData(w http.ResponseWriter, r *http.Request) { + log.Debug("DatabaseUpdateData processing... ") + + body, err := io.ReadAll(io.LimitReader(r.Body, global.RequestBodyMaxLen)) + if err != nil { + log.Error("io.ReadAll is failed:", err) + services.ResponseNotFound404UriNotExist(w, r) + return + } + + vars := mux.Vars(r) + tblName := vars["objectTypeValue"] + wc := services.GetUriWhereString(r) + + log.Debug("Request body:", string(body), "Table name:", tblName, "wc:", wc) + updateData := make(map[string]interface{}) + _ = json.Unmarshal(body, &updateData) + + tn, sql := dborm.ConstructUpdateSQL(tblName, updateData, wc) + log.Tracef("tn: %s sql :%s", tn, sql) + xSession := DbClient.XEngine.NewSession() + defer xSession.Close() + var affected int64 + for _, s := range sql { + res, err := xSession.Exec(s) + if err != nil { + log.Error("Update failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + n, _ := res.RowsAffected() + affected = affected + n + } + xSession.Commit() + mapRow := make(map[string]interface{}) + row := map[string]interface{}{"affectedRows": affected} + mapRow[tn] = row + services.ResponseWithJson(w, http.StatusOK, mapRow) +} + +func DatabaseDeleteData(w http.ResponseWriter, r *http.Request) { + log.Debug("DatabaseDeleteData processing... ") + + vars := mux.Vars(r) + tblName := vars["objectTypeValue"] + wc := services.GetUriWhereString(r) + + log.Debug("Table name:", tblName, "wc:", wc) + + sql := dborm.ConstructDeleteSQL(tblName, wc) + xSession := DbClient.XEngine.NewSession() + defer xSession.Close() + res, err := xSession.Exec(sql) + if err != nil { + log.Error("Update failed, err:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + affected, _ := res.RowsAffected() + xSession.Commit() + mapRow := make(map[string]interface{}) + row := map[string]interface{}{"affectedRows": affected} + mapRow["data"] = row + services.ResponseWithJson(w, http.StatusOK, mapRow) +} + +// 连接用户实例 +func DbConnection(w http.ResponseWriter, r *http.Request) { + if dborm.DbClient.XEngine == nil { + services.ResponseErrorWithJson(w, 400, "无连接") + } + // 查询实例 + result, err := dborm.DbClient.XEngine.QueryString("SHOW PROCESSLIST;") + if err != nil { + services.ResponseErrorWithJson(w, 500, err.Error()) + } + filterData := []map[string]string{} + for _, r := range result { + if r["User"] != "system user" { + filterData = append(filterData, r) + } + } + // Sleep:连接处于空闲状态,没有执行任何操作。 + // Query:连接正在执行一个查询语句。 + // Execute:连接正在执行一个准备好的 SQL 语句。 + // Connect:连接正在建立但尚未完成。 + services.ResponseWithJson(w, 200, filterData) +} + +// 关闭数据库连接 +func DbStop(w http.ResponseWriter, r *http.Request) { + if dborm.DbClient.XEngine == nil { + services.ResponseErrorWithJson(w, 400, "无连接") + } + + // json 請求參數獲取 + var bodyArgs struct { + ID string `json:"ID" validate:"required"` + } + err := ctx.ShouldBindJSON(r, &bodyArgs) + if err != nil { + log.Error("io.ReadAll is failed:", err) + services.ResponseErrorWithJson(w, 400, err.Error()) + return + } + + // 关闭 + rse, err := dborm.DbClient.XEngine.Exec("KILL ?;", bodyArgs.ID) + if err != nil { + services.ResponseErrorWithJson(w, 500, err.Error()) + return + } + services.ResponseWithJson(w, 200, rse) +} + +// xorm Get data from database +func TaskDatabaseGetData(w http.ResponseWriter, r *http.Request) { + log.Debug("DatabaseGetData processing... ") + + var sql []string + var err error + + vars := mux.Vars(r) + tblName := vars["objectTypeValue"] + sql = GetUriSQLArray(r) + // select as must, todo ... + + if sql == nil { + wc := services.GetUriWhereString(r) + if wc == "" { + sql = append(sql, fmt.Sprintf("select * from %s", tblName)) + } else { + sql = append(sql, fmt.Sprintf("select * from %s where %s", tblName, wc)) + } + } + + ls := services.GetUriPageLimitString(r) + + // data := make([]map[string]interface{}, 0) + // xormResponse := make([]map[string]interface{}, len(sql)) + var xormResponse XormResponse + data := make([]map[string]interface{}, 0) + for i, s := range sql { + log.Tracef("SQL[%d]: %s", i, sql[i]) + + rows := make([]map[string]interface{}, 0) + mapRows := make(map[string]interface{}) + + if s != "" { + // err = XEngine.SQL(s).Find(&rows) + if IsQuerySQL(s) == false { + services.ResponseNotAcceptable406QuerySQLError(w) + return + } + + querySQL := s + if i == (len(sql) - 1) { + querySQL = querySQL + " " + ls + } + log.Debug("querySQL:", querySQL) + rows, err = DbClient.XEngine.QueryInterface(querySQL) + if err != nil { + log.Error("SQL failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + tableName := GetTableName(s) + log.Debugf("s:%s tableName:%s", s, tableName) + mapRows[tableName] = rows + data = append(data, mapRows) + log.Trace("data:", data) + } + i++ + } + xormResponse.Data = data + + services.ResponseWithJson(w, http.StatusOK, xormResponse) +} + +func TaskDatabaseInsertData(w http.ResponseWriter, r *http.Request) { + log.Debug("DatabaseInsertData processing... ") + + body, err := io.ReadAll(io.LimitReader(r.Body, global.RequestBodyMaxLen)) //io.LimitReader限制大小 + if err != nil { + log.Error("io.ReadAll failed:", err) + services.ResponseNotFound404UriNotExist(w, r) + return + } + + vars := mux.Vars(r) + tableName := vars["objectTypeValue"] + log.Debug("Request body:", string(body), "tableName:", tableName) + insertData := make(map[string]interface{}) + _ = json.Unmarshal(body, &insertData) + + tn, sql := dborm.ConstructInsertSQL(tableName, insertData) + log.Tracef("tn: %s sql :%s", tn, sql) + xSession := DbClient.XEngine.NewSession() + defer xSession.Close() + var affected int64 + for _, s := range sql { + res, err := xSession.Exec(s) + if err != nil { + log.Error("Insert failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + n, _ := res.RowsAffected() + affected = affected + n + } + xSession.Commit() + // affected, err := InsertDataWithJson(insertData) + mapRow := make(map[string]interface{}) + row := map[string]interface{}{"affectedRows": affected} + mapRow[tn] = row + // xormResponse.Data = mapRow + services.ResponseWithJson(w, http.StatusOK, mapRow) +} + +func TaskDatabaseUpdateData(w http.ResponseWriter, r *http.Request) { + log.Debug("DatabaseUpdateData processing... ") + + body, err := io.ReadAll(io.LimitReader(r.Body, global.RequestBodyMaxLen)) + if err != nil { + log.Error("io.ReadAll is failed:", err) + services.ResponseNotFound404UriNotExist(w, r) + return + } + + vars := mux.Vars(r) + tblName := vars["objectTypeValue"] + wc := services.GetUriWhereString(r) + + log.Debug("Request body:", string(body), "Table name:", tblName, "wc:", wc) + updateData := make(map[string]interface{}) + _ = json.Unmarshal(body, &updateData) + + tn, sql := dborm.ConstructUpdateSQL(tblName, updateData, wc) + log.Tracef("tn: %s sql :%s", tn, sql) + xSession := DbClient.XEngine.NewSession() + defer xSession.Close() + var affected int64 + for _, s := range sql { + res, err := xSession.Exec(s) + if err != nil { + log.Error("Update failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + n, _ := res.RowsAffected() + affected = affected + n + } + xSession.Commit() + mapRow := make(map[string]interface{}) + row := map[string]interface{}{"affectedRows": affected} + mapRow[tn] = row + services.ResponseWithJson(w, http.StatusOK, mapRow) +} + +func TaskDatabaseDeleteData(w http.ResponseWriter, r *http.Request) { + log.Debug("DatabaseDeleteData processing... ") + + vars := mux.Vars(r) + tblName := vars["objectTypeValue"] + wc := services.GetUriWhereString(r) + + log.Debug("Table name:", tblName, "wc:", wc) + + sql := dborm.ConstructDeleteSQL(tblName, wc) + xSession := DbClient.XEngine.NewSession() + defer xSession.Close() + res, err := xSession.Exec(sql) + if err != nil { + log.Error("Update failed, err:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + affected, _ := res.RowsAffected() + xSession.Commit() + mapRow := make(map[string]interface{}) + row := map[string]interface{}{"affectedRows": affected} + mapRow["data"] = row + services.ResponseWithJson(w, http.StatusOK, mapRow) +} diff --git a/features/file/file.go b/features/file/file.go new file mode 100644 index 0000000..815daca --- /dev/null +++ b/features/file/file.go @@ -0,0 +1,194 @@ +package file + +import ( + "fmt" + "net/http" + "path/filepath" + + "ems.agt/lib/core/utils/ctx" + "ems.agt/lib/dborm" + "ems.agt/lib/file" + "ems.agt/lib/log" + "ems.agt/lib/services" + "ems.agt/restagent/config" + "github.com/gorilla/mux" + "github.com/shirou/gopsutil/disk" +) + +var ( + // parameter config management + UriFile = config.DefaultUriPrefix + "/fileManagement/{apiVersion}/{location}/file" + + CustomUriFile = config.UriPrefix + "/fileManagement/{apiVersion}/{location}/file" + + // 获取磁盘列表 + UriDiskList = config.DefaultUriPrefix + "/fileManagement/{apiVersion}/files/diskList" + + // 获取文件列表 + UriListFiles = config.DefaultUriPrefix + "/fileManagement/{apiVersion}/files/listFiles" +) + +// func init() { +// routes.Register("POST", UriFile, UploadFile, nil) +// routes.Register("GET", UriFile, DownloadFile, nil) +// routes.Register("DELETE", UriFile, DeleteFile, nil) +// } + +func UploadFile(w http.ResponseWriter, r *http.Request) { + log.Debug("UploadFile processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Http request error:", err) + return + } + + vars := mux.Vars(r) + location := vars["location"] + if location == "" { + log.Error("location is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + params := r.URL.Query() + rename, ok := params["rename"] + if !ok { + log.Info("rename parameter is not exist") + } + log.Debug("rename:", rename) + + var path, fileName string + if len(rename) > 0 { + fileName = rename[0] + } + if location == "upload" { + path = config.GetYamlConfig().OMC.Upload + fileName, err = services.HandleUploadFile(r, path, fileName) + if err != nil { + log.Error("Faile to HandleUploadFile:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + } else { + path = config.GetYamlConfig().OMC.FrontUpload + fileName, err = services.HandleUploadFile(r, path, fileName) + if err != nil { + log.Error("Faile to HandleUploadFile:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + } + log.Debugf("upload file=%s to path=%s", fileName, path) + + services.ResponseStatusOK204NoContent(w) + return +} + +func DownloadFile(w http.ResponseWriter, r *http.Request) { + log.Debug("DownloadFile processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + vars := mux.Vars(r) + location := vars["location"] + if location == "" { + log.Error("location is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + + // 全路径文件下载 + filePath := r.URL.Query().Get("path") + if location == "path" && filePath != "" { + // 截取文件路径 + dir := filepath.Dir(filePath) + // 截取文件名 + fileName := filepath.Base(filePath) + services.ResponseFileWithNameAndMD5(w, http.StatusOK, fileName, dir, "") + return + } + + params := r.URL.Query() + fileNames, ok := params["loc"] + if !ok { + log.Info("loc parameter is not exist") + } + var path string + if location == "upload" { + path = config.GetYamlConfig().OMC.Upload + } else { + path = config.GetYamlConfig().OMC.FrontUpload + } + for _, fileName := range fileNames { + services.ResponseFileWithNameAndMD5(w, http.StatusOK, fileName, path, "") + } + return +} + +func DeleteFile(w http.ResponseWriter, r *http.Request) { + log.Debug("DeleteFile processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + vars := mux.Vars(r) + neType := vars["neType"] + if neType == "" { + log.Error("neType is empty") + services.ResponseNotFound404UriNotExist(w, r) + return + } + + services.ResponseStatusOK204NoContent(w) + return +} + +// 磁盘列表 +func DiskList(w http.ResponseWriter, r *http.Request) { + disks := make([]map[string]string, 0) + + partitions, err := disk.Partitions(false) + if err != nil { + services.ResponseWithJson(w, 200, disks) + } + + for _, partition := range partitions { + usage, err := disk.Usage(partition.Mountpoint) + if err != nil { + continue + } + disks = append(disks, map[string]string{ + "size": file.FormatFileSize(float64(usage.Total)), + "used": file.FormatFileSize(float64(usage.Used)), + "avail": file.FormatFileSize(float64(usage.Free)), + "pcent": fmt.Sprintf("%.1f%%", usage.UsedPercent), + "target": partition.Device, + }) + } + services.ResponseWithJson(w, 200, disks) +} + +// 获取文件列表 /files/search +func ListFiles(w http.ResponseWriter, r *http.Request) { + // json 請求參數獲取 + var bodyArgs FileOption + err := ctx.ShouldBindJSON(r, &bodyArgs) + if err != nil || dborm.DbClient.XEngine == nil { + services.ResponseErrorWithJson(w, 400, err.Error()) + return + } + + files, err := GetFileList(bodyArgs) + if err != nil { + services.ResponseErrorWithJson(w, 400, err.Error()) + return + } + services.ResponseWithJson(w, 200, files) +} diff --git a/features/file/model.go b/features/file/model.go new file mode 100644 index 0000000..1de351f --- /dev/null +++ b/features/file/model.go @@ -0,0 +1,85 @@ +package file + +import ( + "bufio" + "fmt" + "io/fs" + "os" + "os/exec" + "time" + + "github.com/spf13/afero" +) + +type FileOption struct { + Path string `json:"path"` + Search string `json:"search"` + ContainSub bool `json:"containSub"` + Expand bool `json:"expand"` + Dir bool `json:"dir"` + ShowHidden bool `json:"showHidden"` + Page int `json:"page"` + PageSize int `json:"pageSize"` +} + +type FileInfo struct { + Fs afero.Fs `json:"-"` + Path string `json:"path"` + Name string `json:"name"` + Extension string `json:"extension"` + Content string `json:"content"` + Size int64 `json:"size"` + IsDir bool `json:"isDir"` + IsSymlink bool `json:"isSymlink"` + IsHidden bool `json:"isHidden"` + LinkPath string `json:"linkPath"` + Type string `json:"type"` + Mode string `json:"mode"` + MimeType string `json:"mimeType"` + UpdateTime time.Time `json:"updateTime"` + ModTime time.Time `json:"modTime"` + FileMode os.FileMode `json:"-"` + Items []*FileInfo `json:"items"` + ItemTotal int `json:"itemTotal"` +} + +func (f *FileInfo) search(search string, count int) (files []FileSearchInfo, total int, err error) { + cmd := exec.Command("find", f.Path, "-name", fmt.Sprintf("*%s*", search)) + output, err := cmd.StdoutPipe() + if err != nil { + return + } + if err = cmd.Start(); err != nil { + return + } + defer func() { + _ = cmd.Wait() + _ = cmd.Process.Kill() + }() + + scanner := bufio.NewScanner(output) + for scanner.Scan() { + line := scanner.Text() + info, err := os.Stat(line) + if err != nil { + continue + } + total++ + if total > count { + continue + } + files = append(files, FileSearchInfo{ + Path: line, + FileInfo: info, + }) + } + if err = scanner.Err(); err != nil { + return + } + return +} + +type FileSearchInfo struct { + Path string `json:"path"` + fs.FileInfo +} diff --git a/features/file/service.go b/features/file/service.go new file mode 100644 index 0000000..9fc3ec6 --- /dev/null +++ b/features/file/service.go @@ -0,0 +1,206 @@ +package file + +import ( + "errors" + "fmt" + "os" + "path" + "path/filepath" + "strings" + + "ems.agt/lib/file" + "github.com/spf13/afero" +) + +// 获取文件列表 +func GetFileList(op FileOption) (FileInfo, error) { + var fileInfo FileInfo + if _, err := os.Stat(op.Path); err != nil && os.IsNotExist(err) { + return fileInfo, nil + } + info, err := NewFileInfo(op) + if err != nil { + return fileInfo, err + } + fileInfo = *info + return fileInfo, nil +} + +func NewFileInfo(op FileOption) (*FileInfo, error) { + var appFs = afero.NewOsFs() + + info, err := appFs.Stat(op.Path) + if err != nil { + return nil, err + } + + fileInfo := &FileInfo{ + Fs: appFs, + Path: op.Path, + Name: info.Name(), + IsDir: info.IsDir(), + FileMode: info.Mode(), + ModTime: info.ModTime(), + Size: info.Size(), + IsSymlink: file.IsSymlink(info.Mode()), + Extension: filepath.Ext(info.Name()), + IsHidden: file.IsHidden(op.Path), + Mode: fmt.Sprintf("%04o", info.Mode().Perm()), + MimeType: file.GetMimeType(op.Path), + } + if fileInfo.IsSymlink { + fileInfo.LinkPath = file.GetSymlink(op.Path) + } + if op.Expand { + if fileInfo.IsDir { + if err := listChildren(fileInfo, op.Dir, op.ShowHidden, op.ContainSub, op.Search, op.Page, op.PageSize); err != nil { + return nil, err + } + return fileInfo, nil + } else { + if err := getContent(fileInfo); err != nil { + return nil, err + } + } + } + return fileInfo, nil +} + +func listChildren(f *FileInfo, dir, showHidden, containSub bool, search string, page, pageSize int) error { + afs := &afero.Afero{Fs: f.Fs} + var ( + files []FileSearchInfo + err error + total int + ) + + if search != "" && containSub { + files, total, err = f.search(search, page*pageSize) + if err != nil { + return err + } + } else { + dirFiles, err := afs.ReadDir(f.Path) + if err != nil { + return err + } + for _, file := range dirFiles { + files = append(files, FileSearchInfo{ + Path: f.Path, + FileInfo: file, + }) + } + } + + var items []*FileInfo + for _, df := range files { + if dir && !df.IsDir() { + continue + } + name := df.Name() + fPath := path.Join(df.Path, df.Name()) + if search != "" { + if containSub { + fPath = df.Path + name = strings.TrimPrefix(strings.TrimPrefix(fPath, f.Path), "/") + } else { + lowerName := strings.ToLower(name) + lowerSearch := strings.ToLower(search) + if !strings.Contains(lowerName, lowerSearch) { + continue + } + } + } + if !showHidden && file.IsHidden(name) { + continue + } + f.ItemTotal++ + isSymlink, isInvalidLink := false, false + if file.IsSymlink(df.Mode()) { + isSymlink = true + info, err := f.Fs.Stat(fPath) + if err == nil { + df.FileInfo = info + } else { + isInvalidLink = true + } + } + + fileInfo := &FileInfo{ + Fs: f.Fs, + Name: name, + Size: df.Size(), + ModTime: df.ModTime(), + FileMode: df.Mode(), + IsDir: df.IsDir(), + IsSymlink: isSymlink, + IsHidden: file.IsHidden(fPath), + Extension: filepath.Ext(name), + Path: fPath, + Mode: fmt.Sprintf("%04o", df.Mode().Perm()), + } + + if isSymlink { + fileInfo.LinkPath = file.GetSymlink(fPath) + } + if df.Size() > 0 { + fileInfo.MimeType = file.GetMimeType(fPath) + } + if isInvalidLink { + fileInfo.Type = "invalid_link" + } + items = append(items, fileInfo) + } + if containSub { + f.ItemTotal = total + } + start := (page - 1) * pageSize + end := pageSize + start + var result []*FileInfo + if start < 0 || start > f.ItemTotal || end < 0 || start > end { + result = items + } else { + if end > f.ItemTotal { + result = items[start:] + } else { + result = items[start:end] + } + } + + f.Items = result + return nil +} + +func getContent(f *FileInfo) error { + if f.Size <= 10*1024*1024 { + afs := &afero.Afero{Fs: f.Fs} + cByte, err := afs.ReadFile(f.Path) + if err != nil { + return nil + } + if len(cByte) > 0 && detectBinary(cByte) { + return errors.New("ErrFileCanNotRead") + } + f.Content = string(cByte) + return nil + } else { + return errors.New("ErrFileCanNotRead") + } +} + +func detectBinary(buf []byte) bool { + whiteByte := 0 + n := 1024 + if len(buf) < 1024 { + n = len(buf) + } + for i := 0; i < n; i++ { + if (buf[i] >= 0x20) || buf[i] == 9 || buf[i] == 10 || buf[i] == 13 { + whiteByte++ + } else if buf[i] <= 6 || (buf[i] >= 14 && buf[i] <= 31) { + return true + } + } + + return whiteByte < 1 +} diff --git a/features/firewall/api_firewall.go b/features/firewall/api_firewall.go new file mode 100644 index 0000000..136f3b6 --- /dev/null +++ b/features/firewall/api_firewall.go @@ -0,0 +1,88 @@ +package firewall + +import ( + "net/http" + + "ems.agt/features/firewall/model" + "ems.agt/features/firewall/service" + "ems.agt/lib/core/utils/ctx" + "ems.agt/lib/core/vo/result" + "ems.agt/lib/services" + "ems.agt/restagent/config" +) + +// 防火墙管理接口添加到路由 +func Routers() []services.RouterItem { + // 实例化控制层 FirewallApi 结构体 + var apis = &FirewallApi{ + firewallService: *service.NewServiceFirewall, + } + + rs := [...]services.RouterItem{ + { + Method: "GET", + Pattern: "/base", + Handler: apis.BaseInfo, + Middleware: nil, //midware.Authorize(nil), + }, + { + Method: "POST", + Pattern: "/rule", + Handler: apis.Rule, + Middleware: nil, //midware.Authorize(nil), + }, + // 添加更多的 Router 对象... + } + + // 生成两组前缀路由 + rsPrefix := []services.RouterItem{} + for _, v := range rs { + path := "/firewallManage/{apiVersion}" + v.Pattern + // 固定前缀 + v.Pattern = config.DefaultUriPrefix + path + rsPrefix = append(rsPrefix, v) + // 可配置 + v.Pattern = config.UriPrefix + path + rsPrefix = append(rsPrefix, v) + } + return rsPrefix +} + +// 防火墙管理 +// +// PATH /firewallManage +type FirewallApi struct { + firewallService service.ServiceFirewall +} + +// 获取防火墙基础信息 +// +// GET /base +func (s *FirewallApi) BaseInfo(w http.ResponseWriter, r *http.Request) { + data, err := s.firewallService.LoadBaseInfo() + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + ctx.JSON(w, 200, result.OkData(data)) +} + +// 获取防火墙规则列表分页 +// +// GET /rule +func (s *FirewallApi) Rule(w http.ResponseWriter, r *http.Request) { + var body model.RuleQuerys + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.Type == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + data, err := s.firewallService.RulePage(body) + if err != nil { + ctx.JSON(w, 400, result.ErrMsg(err.Error())) + return + } + + ctx.JSON(w, 200, result.OkData(data)) +} diff --git a/features/firewall/model/firewall.go b/features/firewall/model/firewall.go new file mode 100644 index 0000000..782aa89 --- /dev/null +++ b/features/firewall/model/firewall.go @@ -0,0 +1,13 @@ +package model + +type Firewall struct { + ID int64 `json:"id" xorm:"id"` + CreatedAt int64 `json:"createdAt" xorm:"created_at"` + UpdatedAt int64 `json:"updatedAt" xorm:"updated_at"` + Type string `json:"type" xorm:"type"` + Port string `json:"port" xorm:"port"` + Protocol string `json:"protocol" xorm:"protocol"` + Address string `json:"address" xorm:"address"` + Strategy string `json:"strategy" xorm:"strategy"` + Description string `json:"description" xorm:"description"` +} diff --git a/features/firewall/model/firewall_vo.go b/features/firewall/model/firewall_vo.go new file mode 100644 index 0000000..d4e3553 --- /dev/null +++ b/features/firewall/model/firewall_vo.go @@ -0,0 +1,64 @@ +package model + +type FirewallBaseInfo struct { + Name string `json:"name"` + Status string `json:"status"` + Version string `json:"version"` + PingStatus string `json:"pingStatus"` +} + +type RuleQuerys struct { + PageNum int `json:"pageNum" validate:"required,number"` + PageSize int `json:"pageSize" validate:"required,number"` + Info string `json:"info"` + Status string `json:"status"` + Strategy string `json:"strategy"` + Type string `json:"type" validate:"required"` +} + +type FirewallOperation struct { + Operation string `json:"operation" validate:"required,oneof=start stop disablePing enablePing"` +} + +type PortRuleOperate struct { + Operation string `json:"operation" validate:"required,oneof=add remove"` + Address string `json:"address"` + Port string `json:"port" validate:"required"` + Protocol string `json:"protocol" validate:"required,oneof=tcp udp tcp/udp"` + Strategy string `json:"strategy" validate:"required,oneof=accept drop"` + + Description string `json:"description"` +} + +type UpdateFirewallDescription struct { + Type string `json:"type"` + Address string `json:"address"` + Port string `json:"port"` + Protocol string `json:"protocol"` + Strategy string `json:"strategy" validate:"required,oneof=accept drop"` + + Description string `json:"description"` +} + +type AddrRuleOperate struct { + Operation string `json:"operation" validate:"required,oneof=add remove"` + Address string `json:"address" validate:"required"` + Strategy string `json:"strategy" validate:"required,oneof=accept drop"` + + Description string `json:"description"` +} + +type PortRuleUpdate struct { + OldRule PortRuleOperate `json:"oldRule"` + NewRule PortRuleOperate `json:"newRule"` +} + +type AddrRuleUpdate struct { + OldRule AddrRuleOperate `json:"oldRule"` + NewRule AddrRuleOperate `json:"newRule"` +} + +type BatchRuleOperate struct { + Type string `json:"type" validate:"required"` + Rules []PortRuleOperate `json:"rules"` +} diff --git a/features/firewall/repo/repo_firewall.go b/features/firewall/repo/repo_firewall.go new file mode 100644 index 0000000..e95a056 --- /dev/null +++ b/features/firewall/repo/repo_firewall.go @@ -0,0 +1,133 @@ +package repo + +import ( + "strings" + + "ems.agt/features/firewall/model" + "ems.agt/lib/core/datasource" + "ems.agt/lib/log" +) + +// 实例化数据层 RepoFirewall 结构体 +var NewRepoFirewall = &RepoFirewall{ + selectSql: `select + id, created_at, updated_at, type, port, protocol, address, strategy, description + from monitor_firewall`, + + resultMap: map[string]string{ + "id": "ID", + "created_at": "CreatedAt", + "updated_at": "UpdatedAt", + "type": "Type", + "port": "Port", + "protocol": "Protocol", + "address": "Address", + "strategy": "Strategy", + "description": "Description", + }, +} + +// RepoFirewall 防火墙 数据层处理 +type RepoFirewall struct { + // 查询视图对象SQL + selectSql string + // 结果字段与实体映射 + resultMap map[string]string +} + +// convertResultRows 将结果记录转实体结果组 +func (r *RepoFirewall) convertResultRows(rows []map[string]any) []model.Firewall { + arr := make([]model.Firewall, 0) + for _, row := range rows { + UdmUser := model.Firewall{} + for key, value := range row { + if keyMapper, ok := r.resultMap[key]; ok { + datasource.SetFieldValue(&UdmUser, keyMapper, value) + } + } + arr = append(arr, UdmUser) + } + return arr +} + +// List 根据实体查询 +func (r *RepoFirewall) List(f model.Firewall) []model.Firewall { + // 查询条件拼接 + var conditions []string + var params []any + if f.Type != "" { + conditions = append(conditions, "type = ?") + params = append(params, f.Type) + } + if f.Protocol != "" { + conditions = append(conditions, "protocol = ?") + params = append(params, f.Protocol) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询数据 + querySql := r.selectSql + whereSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + log.Errorf("query err => %v", err) + } + + // 转换实体 + return r.convertResultRows(results) +} + +// Insert 新增实体 +func (r *RepoFirewall) Insert(f model.Firewall) int64 { + results, err := datasource.DefaultDB().Table("monitor_firewall").Insert(f) + if err != nil { + return results + } + return results +} + +// Update 修改更新 +func (r *RepoFirewall) Update(f model.Firewall) int64 { + // 查询先 + var fd model.Firewall + if f.Type == "port" { + has, err := datasource.DefaultDB().Table("monitor_firewall").Where("type = ? AND port = ? AND protocol = ? AND address = ? AND strategy = ?", "port", f.Port, f.Protocol, f.Address, f.Strategy).Get(&fd) + if !has || err != nil { + return 0 + } + } else { + has, err := datasource.DefaultDB().Table("monitor_firewall").Where("type = ? AND address = ? AND strategy = ?", "address", f.Address, f.Strategy).Get(&fd) + if !has || err != nil { + return 0 + } + } + f.ID = fd.ID + + results, err := datasource.DefaultDB().Table("monitor_firewall").Where("id = ?", f.ID).Update(f) + if err != nil { + return 0 + } + return results +} + +// Delete 删除实体 +func (r *RepoFirewall) Delete(id int64) int64 { + results, err := datasource.DefaultDB().Table("u_sub_user").Where("id = ?", id).Delete() + if err != nil { + return results + } + return results +} + +// DeleteFirewallRecord 删除实体 +func (r *RepoFirewall) DeleteFirewallRecord(fType, port, protocol, address, strategy string) int64 { + results, err := datasource.DefaultDB().Table("u_sub_user").Where("type = ? AND port = ? AND protocol = ? AND address = ? AND strategy = ?", fType, port, protocol, address, strategy).Delete() + if err != nil { + return results + } + return results +} diff --git a/features/firewall/service/service_firewall.go b/features/firewall/service/service_firewall.go new file mode 100644 index 0000000..62579a2 --- /dev/null +++ b/features/firewall/service/service_firewall.go @@ -0,0 +1,207 @@ +package service + +import ( + "fmt" + "os" + "strconv" + "strings" + + "ems.agt/features/firewall/model" + "ems.agt/features/firewall/repo" + "ems.agt/lib/core/cmd" + "ems.agt/lib/core/utils/firewall" + fireClient "ems.agt/lib/core/utils/firewall/client" + "ems.agt/lib/core/utils/scan" +) + +// 实例化服务层 ServiceFirewall 结构体 +var NewServiceFirewall = &ServiceFirewall{ + repoFirewall: *repo.NewRepoFirewall, +} + +// ServiceFirewall 防火墙 服务层处理 +type ServiceFirewall struct { + repoFirewall repo.RepoFirewall +} + +// LoadBaseInfo 获取防火墙基础信息 +func (s *ServiceFirewall) LoadBaseInfo() (model.FirewallBaseInfo, error) { + var baseInfo model.FirewallBaseInfo + baseInfo.PingStatus = s.pingStatus() + baseInfo.Status = "not running" + baseInfo.Version = "-" + baseInfo.Name = "-" + client, err := firewall.NewFirewallClient() + if err != nil { + if err.Error() == "no such type" { + return baseInfo, nil + } + return baseInfo, err + } + baseInfo.Name = client.Name() + baseInfo.Status, err = client.Status() + if err != nil { + return baseInfo, err + } + if baseInfo.Status == "not running" { + return baseInfo, err + } + baseInfo.Version, err = client.Version() + if err != nil { + return baseInfo, err + } + return baseInfo, nil +} + +// LoadBaseInfo 获取防火墙基础信息 +func (s *ServiceFirewall) RulePage(querys model.RuleQuerys) (map[string]any, error) { + var ( + datas []fireClient.FireInfo + backDatas []fireClient.FireInfo + ) + + data := map[string]any{ + "total": 0, + "rows": backDatas, + } + + client, err := firewall.NewFirewallClient() + if err != nil { + return data, err + } + if querys.Type == "port" { + ports, err := client.ListPort() + if err != nil { + return data, err + } + if len(querys.Info) != 0 { + for _, port := range ports { + if strings.Contains(port.Port, querys.Info) { + datas = append(datas, port) + } + } + } else { + datas = ports + } + } else { + addrs, err := client.ListAddress() + if err != nil { + return data, err + } + if len(querys.Info) != 0 { + for _, addr := range addrs { + if strings.Contains(addr.Address, querys.Info) { + datas = append(datas, addr) + } + } + } else { + datas = addrs + } + } + + var datasFilterStatus []fireClient.FireInfo + if len(querys.Status) != 0 { + for _, data := range datas { + portItem, _ := strconv.Atoi(data.Port) + if querys.Status == "free" && !scan.ScanPortWithProto(portItem, data.Protocol) { + datasFilterStatus = append(datasFilterStatus, data) + } + if querys.Status == "used" && scan.ScanPortWithProto(portItem, data.Protocol) { + datasFilterStatus = append(datasFilterStatus, data) + } + } + } else { + datasFilterStatus = datas + } + var datasFilterStrategy []fireClient.FireInfo + if len(querys.Strategy) != 0 { + for _, data := range datasFilterStatus { + if querys.Strategy == data.Strategy { + datasFilterStrategy = append(datasFilterStrategy, data) + } + } + } else { + datasFilterStrategy = datasFilterStatus + } + + total, start, end := len(datasFilterStrategy), (querys.PageNum-1)*querys.PageSize, querys.PageNum*querys.PageSize + if start > total { + backDatas = make([]fireClient.FireInfo, 0) + } else { + if end >= total { + end = total + } + backDatas = datasFilterStrategy[start:end] + } + + datasFromDB := s.repoFirewall.List(model.Firewall{}) + for i := 0; i < len(backDatas); i++ { + for _, des := range datasFromDB { + if querys.Type != des.Type { + continue + } + if backDatas[i].Port == des.Port && querys.Type == "port" && + backDatas[i].Protocol == des.Protocol && + backDatas[i].Strategy == des.Strategy && + backDatas[i].Address == des.Address { + backDatas[i].Description = des.Description + break + } + if querys.Type == "address" && backDatas[i].Strategy == des.Strategy && backDatas[i].Address == des.Address { + backDatas[i].Description = des.Description + break + } + } + } + + if querys.Type == "port" { + for i := 0; i < len(backDatas); i++ { + port, _ := strconv.Atoi(backDatas[i].Port) + backDatas[i].IsUsed = scan.ScanPort(port) + if backDatas[i].Protocol == "udp" { + backDatas[i].IsUsed = scan.ScanUDPPort(port) + continue + } + } + } + go s.cleanUnUsedData(client) + + return data, nil +} + +func (s *ServiceFirewall) pingStatus() string { + if _, err := os.Stat("/etc/sysctl.conf"); err != nil { + return "None" + } + sudo := cmd.SudoHandleCmd() + command := fmt.Sprintf("%s cat /etc/sysctl.conf | grep net/ipv4/icmp_echo_ignore_all= ", sudo) + stdout, _ := cmd.Exec(command) + if stdout == "net/ipv4/icmp_echo_ignore_all=1\n" { + return "Enable" + } + return "Disable" +} + +func (s *ServiceFirewall) cleanUnUsedData(client firewall.FirewallClient) { + list, _ := client.ListPort() + addressList, _ := client.ListAddress() + list = append(list, addressList...) + if len(list) == 0 { + return + } + records := s.repoFirewall.List(model.Firewall{}) + if len(records) == 0 { + return + } + for _, item := range list { + for i := 0; i < len(records); i++ { + if records[i].Port == item.Port && records[i].Protocol == item.Protocol && records[i].Strategy == item.Strategy && records[i].Address == item.Address { + records = append(records[:i], records[i+1:]...) + } + } + } + + for _, record := range records { + _ = s.repoFirewall.Delete(record.ID) + } +} diff --git a/features/fm/alarm.go b/features/fm/alarm.go new file mode 100644 index 0000000..6a317fa --- /dev/null +++ b/features/fm/alarm.go @@ -0,0 +1,700 @@ +package fm + +import ( + "database/sql" + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + "strings" + "time" + + "ems.agt/lib/dborm" + "ems.agt/lib/global" + "ems.agt/lib/log" + "ems.agt/lib/services" + "ems.agt/restagent/config" + "xorm.io/xorm" + + "github.com/go-resty/resty/v2" + _ "github.com/go-sql-driver/mysql" + "github.com/gorilla/mux" +) + +const ( + AlarmStatusClear = 0 + AlarmStatusActive = 1 +) + +const ( + ClearTypeUnclear = 0 + ClearTypeAutoClear = 1 + ClearTypeManualClear = 2 +) + +const ( + AckStateUnacked = 0 + AckStateAcked = 1 +) + +const ( + AlarmTypeCommunicationAlarm = 1 + AlarmTypeEquipmentAlarm = 2 + AlarmTypeProcessingFailure = 3 + AlarmTypeEnvironmentalAlarm = 4 + AlarmTypeQualityOfServiceAlarm = 5 +) + +const ( + AlarmPerceivedSeverityCritical = 1 + AlarmPerceivedSeverityMajor = 2 + AlarmPerceivedSeverityMinor = 3 + AlarmPerceivedSeverityWarning = 4 + AlarmPerceivedSeverityEvent = 5 +) + +const ( + AlarmSeqBeginNumber = 1 +) + +type Response struct { + Data interface{} `json:"data"` +} + +type Alarm struct { + AlarmSeq int `json:"alarmSeq"` + AlarmId string `json:"alarmId" xorm:"alarm_id"` + NeId string `json:"neId"` + AlarmCode int `json:"alarmCode"` + AlarmTitle string `json:"alarmTitle"` + EventTime string `json:"eventTime"` + AlarmType string `json:"alarmType"` + OrigSeverity string `json:"origSeverity"` + PerceivedSeverity string `json:"perceivedSeverity"` + PVFlag string `json:"pvFlag" xorm:"pv_flag"` + NeName string `json:"neName"` + NeType string `json:"neType"` + ObjectUid string `json:"objectUid" xorm:"object_uid"` + ObjectName string `json:"objectName" xorm:"object_name"` + ObjectType string `json:"objectType" xorm:"object_type"` + LocationInfo string `json:"locationInfo"` + Province string `json:"province"` + AlarmStatus int `json:"alarmStatus"` + SpecificProblem string `json:"specificProblem"` + SpecificProblemID string `json:"specificProblemID" xorm:"specific_problem_id"` + AddInfo string `json:"addInfo"` + + AckState int `json:"ackState"` + AckTime sql.NullTime `json:"ackTime"` + AckUser string `json:"ackUser"` + ClearType int `json:"clearType"` // 0: Unclear, 1: Auto clear, 2: Manual clear + ClearTime sql.NullTime `json:"clearTime"` +} + +type AlarmLog struct { + NeType string `json:"neType" xorm:"ne_type"` + NeId string `json:"neId" xorm:"ne_id"` + AlarmSeq int `json:"alarmSeq" xorm:"alarm_seq"` + AlarmId string `json:"alarmId" xorm:"alarm_id"` + AlarmCode int `json:"alarmCode" xorm:"alarm_code"` + AlarmStatus int `json:"alarmStatus" xorm:"alarm_status"` + EventTime string `json:"eventTime" xorm:"event_time"` + // ClearTime sql.NullTime `json:"clearTime" xorm:"clear_time"` + LogTime string `json:"logTime" xorm:"-"` +} + +var ( + // alarm management + UriAlarms = config.DefaultUriPrefix + "/faultManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/alarms" + UriAlarmsFmt = config.DefaultUriPrefix + "/faultManagement/v1/elementType/%s/objectType/alarms" + + CustomUriAlarms = config.UriPrefix + "/faultManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/alarms" + CustomUriAlarmsFmt = config.UriPrefix + "/faultManagement/v1/elementType/%s/objectType/alarms" +) + +var xEngine *xorm.Engine + +type DatabaseClient struct { + dbType string + dbUrl string + dbConnMaxLifetime time.Duration + dbMaxIdleConns int + dbMaxOpenConns int + IsShowSQL bool + + XEngine *xorm.Engine +} + +var DbClient DatabaseClient + +func InitDbClient(dbType, dbUser, dbPassword, dbHost, dbPort, dbName string) error { + DbClient.dbUrl = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=true&loc=Local", + dbUser, dbPassword, dbHost, dbPort, dbName) + DbClient.dbType = dbType + DbClient.dbConnMaxLifetime = 0 + DbClient.dbMaxIdleConns = 0 + DbClient.dbMaxOpenConns = 0 + if log.GetLevel() == log.LOG_TRACE { + DbClient.IsShowSQL = true + } + log.Debugf("dbType:%s dbUrl:%s:******@tcp(%s:%s)/%s??charset=utf8&parseTime=true&loc=Local", + dbType, dbUser, dbHost, dbPort, dbName) + + var err error + DbClient.XEngine, err = xorm.NewEngine(DbClient.dbType, DbClient.dbUrl) + if err != nil { + log.Error("Failed to connet database:", err) + return err + } + DbClient.XEngine.SetConnMaxLifetime(DbClient.dbConnMaxLifetime) + DbClient.XEngine.SetMaxIdleConns(DbClient.dbMaxIdleConns) + DbClient.XEngine.SetMaxOpenConns(DbClient.dbMaxOpenConns) + if DbClient.IsShowSQL { + DbClient.XEngine.ShowSQL(true) + } + xEngine = DbClient.XEngine + + return nil +} + +func XormConnectDatabase(dbType, dbUser, dbPassword, dbHost, dbPort, dbName string) (*xorm.Engine, error) { + sqlStr := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=true&loc=Local", + dbUser, dbPassword, dbHost, dbPort, dbName) + log.Debugf("dbType:%s Connect to:%s:******@tcp(%s:%s)/%s?charset=utf8&parseTime=true&loc=Local", + dbType, dbUser, dbHost, dbPort, dbName) + var err error + xEngine, err = xorm.NewEngine(dbType, sqlStr) //1、Create xorm engine + if err != nil { + log.Error("Failed to connect database:", err) + return nil, err + } + if log.GetLevel() == log.LOG_TRACE { + xEngine.ShowSQL(true) + } + return xEngine, nil +} + +func IsNeedToAckAlarm(valueJson *dborm.ValueJson, alarm *Alarm) bool { + log.Info("IsNeedToAckAlarm processing... ") + if valueJson != nil { + status, _ := strconv.Atoi(valueJson.AlarmStatus) + log.Tracef("alarm.AlarmStatus=%d, alarm.AlarmType=%s, alarm.OrigSeverity=%s", alarm.AlarmStatus, alarm.AlarmType, alarm.OrigSeverity) + log.Tracef("status=%d, valueJson.AlarmType=%s, valueJson.OrigSeverity=%s", status, valueJson.AlarmType, valueJson.OrigSeverity) + + if alarm.AlarmStatus == status && + alarm.AlarmType == valueJson.AlarmType && + alarm.OrigSeverity == valueJson.OrigSeverity { + return true + } + } + + return false +} + +func SetAlarmAckInfo(valueJson *dborm.ValueJson, alarm *Alarm) { + log.Info("SetAlarmAckInfo processing... ") + alarm.AckState = AckStateAcked + alarm.AckTime.Valid = true + alarm.AckTime.Time = time.Now() + alarm.AckUser = valueJson.AckUser +} + +// process alarm post message from NFs +func PostAlarmFromNF(w http.ResponseWriter, r *http.Request) { + log.Debug("PostAlarmFromNF processing... ") + + vars := mux.Vars(r) + apiVer := vars["apiVersion"] + if apiVer != global.ApiVersionV1 { + log.Error("Uri api version is invalid. apiVersion:", apiVer) + services.ResponseNotFound404UriNotExist(w, r) + return + } + + body, err := io.ReadAll(io.LimitReader(r.Body, global.RequestBodyMaxLen)) + if err != nil { + log.Error("io.ReadAll is failed:", err) + services.ResponseNotFound404UriNotExist(w, r) + return + } + + log.Debug("Request body:", string(body)) + alarmArray := new([]Alarm) + + err = json.Unmarshal(body, &alarmArray) + if err != nil { + log.Error("Failed to Unmarshal:", err) + services.ResponseBadRequest400InvalidJson(w) + return + } + + valueJson, err := dborm.XormGetAAConfig() + if err != nil { + log.Error("Failed to XormGetAAConfig:", err) + //services.ResponseInternalServerError500ProcessError(w, err) + //return + } + log.Trace("valueJson:", valueJson) + // session := xEngine.NewSession() + // defer session.Close() + var activeAlarmNum int = 0 + for _, alarmData := range *alarmArray { + log.Debug("alarmData:", alarmData) + + session := xEngine.NewSession() + defer session.Close() + if alarmData.AlarmStatus == AlarmStatusClear { + alarmData.ClearType = ClearTypeAutoClear + alarmData.ClearTime.Valid = true + tm, _ := time.Parse(time.RFC3339, alarmData.EventTime) + log.Debugf("EventTime:%s tm:%d tm-datetime:%s", alarmData.EventTime, tm, tm.Local().Format(time.DateTime)) + alarmData.ClearTime.Time = tm + if IsNeedToAckAlarm(valueJson, &alarmData) == true { + SetAlarmAckInfo(valueJson, &alarmData) + affected, err := session.Where("ne_type=? and ne_id=? and alarm_id=? and alarm_status=1", alarmData.NeType, alarmData.NeId, alarmData.AlarmId). + Cols("alarm_status", "clear_type", "clear_time", "ack_state", "ack_time", "ack_user"). + Update(alarmData) + if err != nil && affected <= 0 { + log.Error("Failed to update alarm data:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + continue + } + } else { + affected, err := session.Where("ne_type=? and ne_id=? and alarm_id=? and alarm_status=1", alarmData.NeType, alarmData.NeId, alarmData.AlarmId). + Cols("alarm_status", "clear_type", "clear_time"). + Update(alarmData) + if err != nil && affected <= 0 { + log.Error("Failed to update alarm data:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + continue + } + } + log.Trace("alarmData:", alarmData) + var currentSeq string + var seq int + has, err := xEngine.Table("alarm"). + Where("ne_type=? and ne_id=?", alarmData.NeType, alarmData.NeId). + Desc("alarm_seq"). + Cols("alarm_seq"). + Limit(1). + Get(¤tSeq) + if err != nil { + log.Error("Failed to get alarm:", err) + continue + } + if has == true { + seq, _ = strconv.Atoi(currentSeq) + } + + eventTime := global.GetFmtTimeString(time.RFC3339, alarmData.EventTime, time.DateTime) + alarmLog := new(AlarmLog) + alarmLog.NeType = alarmData.NeType + alarmLog.NeId = alarmData.NeId + alarmLog.AlarmSeq = seq + alarmLog.AlarmId = alarmData.AlarmId + alarmLog.AlarmCode = alarmData.AlarmCode + alarmLog.AlarmStatus = alarmData.AlarmStatus + alarmLog.EventTime = eventTime + log.Debug("alarmLog:", alarmLog) + + affected, err := session.Insert(alarmLog) + if err != nil && affected <= 0 { + log.Error("Failed to insert alarm_log:", err) + } + + // todo: PerceivedSeverity set color + var severity string + has, err = xEngine.Table("alarm"). + Where("ne_type=? and ne_id=? and event_time=? and alarm_status=1", alarmData.NeType, alarmData.NeId, alarmData.EventTime). + //OrderBy("FIELD(orig_severity, 'Critical', 'Major', 'Minor', 'Warning', 'Event') ASC"). + Asc("orig_severity"). + Cols("orig_severity"). + Limit(1). + Get(&severity) + if err != nil { + log.Error("Failed to get alarm:", err) + continue + } + log.Debugf("neType=%s, neId=%s, eventTime=%s, severity=%s", alarmData.NeType, alarmData.NeId, alarmData.EventTime, severity) + + if has == true && severity > alarmData.OrigSeverity { + // update exist record + _, err := session.Table("alarm"). + Where("ne_type=? and ne_id=? and event_time=? and alarm_status=1", alarmData.NeType, alarmData.NeId, alarmData.EventTime). + Update(&Alarm{PerceivedSeverity: severity}) + if err != nil { + log.Error("Failed to update alarm:", err) + continue + } + } + session.Commit() + // for alarm forward time format + alarmData.EventTime = eventTime + } else { + activeAlarmNum++ + has, err := xEngine.Table("alarm"). + Where("alarm_id=? and ne_type=? and ne_id=? and alarm_status=1", + alarmData.AlarmId, alarmData.NeType, alarmData.NeId). + Exist() + if err == nil && has == true { + log.Warn("Exist the same alarm") + continue + } + + var currentSeq string + has, err = xEngine.Table("alarm"). + Where("ne_type=? and ne_id=?", alarmData.NeType, alarmData.NeId). + Desc("alarm_seq"). + //Desc("event_time"). + Cols("alarm_seq"). + Limit(1). + Get(¤tSeq) + if err != nil { + log.Error("Failed to get alarm:", err) + continue + } + log.Debugf("neType=%s, neId=%s, currentSeq=%s activeAlarmNum=%d", + alarmData.NeType, alarmData.NeId, currentSeq, activeAlarmNum) + + if has == true { + seq, _ := strconv.Atoi(currentSeq) + alarmData.AlarmSeq = seq + 1 + if alarmData.AlarmSeq > global.MaxInt32Number { + alarmData.AlarmSeq = AlarmSeqBeginNumber + } + } else { + alarmData.AlarmSeq = AlarmSeqBeginNumber + } + + // todo: PerceivedSeverity set color + var severity string + has, err = xEngine.Table("alarm"). + Where("ne_type=? and ne_id=? and event_time=? and alarm_status=1", alarmData.NeType, alarmData.NeId, alarmData.EventTime). + //OrderBy("FIELD(orig_severity, 'Critical', 'Major', 'Minor', 'Warning', 'Event') ASC"). + Asc("orig_severity"). + Cols("orig_severity"). + Limit(1). + Get(&severity) + if err != nil { + log.Error("Failed to get alarm:", err) + continue + } + log.Debugf("neType=%s, neId=%s, eventTime=%s, severity=%s", alarmData.NeType, alarmData.NeId, alarmData.EventTime, severity) + + if has == false || severity == alarmData.OrigSeverity { + alarmData.PerceivedSeverity = alarmData.OrigSeverity + } else if severity > alarmData.OrigSeverity { + alarmData.PerceivedSeverity = alarmData.OrigSeverity + } else { + alarmData.PerceivedSeverity = severity + // update exist record + _, err := session.Table("alarm"). + Where("ne_type=? and ne_id=? and event_time=? and alarm_status=1", alarmData.NeType, alarmData.NeId, alarmData.EventTime). + Update(&Alarm{PerceivedSeverity: alarmData.PerceivedSeverity}) + if err != nil { + log.Error("Failed to update alarm:", err) + continue + } + } + eventTime := global.GetFmtTimeString(time.RFC3339, alarmData.EventTime, time.DateTime) + alarmData.ObjectUid = alarmData.NeId + alarmData.ObjectType = "VNFM" + alarmData.EventTime = eventTime + if alarmData.LocationInfo == "" { + alarmData.LocationInfo = fmt.Sprintf("Host:%s", r.RemoteAddr) + } + if alarmData.AddInfo == "" { + alarmData.LocationInfo = fmt.Sprintf("subNeInfo:%s", alarmData.NeType) + } + if IsNeedToAckAlarm(valueJson, &alarmData) == true { + SetAlarmAckInfo(valueJson, &alarmData) + } + log.Debug("alarmData:", alarmData) + affected, err := session.Insert(alarmData) + if err != nil && affected <= 0 { + log.Error("Failed to insert alarm data:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + continue + } + alarmLog := new(AlarmLog) + alarmLog.NeType = alarmData.NeType + alarmLog.NeId = alarmData.NeId + alarmLog.AlarmSeq = alarmData.AlarmSeq + alarmLog.AlarmId = alarmData.AlarmId + alarmLog.AlarmCode = alarmData.AlarmCode + alarmLog.AlarmStatus = alarmData.AlarmStatus + alarmLog.EventTime = eventTime + log.Trace("alarmLog:", alarmLog) + + affected, err = session.Insert(alarmLog) + if err != nil && affected <= 0 { + log.Error("Failed to insert alarm_log:", err) + } + session.Commit() + } + if config.GetYamlConfig().Alarm.ForwardAlarm { + if err = AlarmEmailForward(&alarmData); err != nil { + log.Error("Failed to AlarmEmailForward:", err) + } + if err = AlarmForwardBySMS(&alarmData); err != nil { + log.Error("Failed to AlarmForwardBySMS:", err) + } + } + } + + services.ResponseStatusOK200Null(w) +} + +// process alarm get from NFs +func GetAlarmFromNF(w http.ResponseWriter, r *http.Request) { + log.Debug("GetAlarmFromNF processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + //var neInfo *dborm.NeInfo + var nes []dborm.NeInfo + _, err = dborm.XormGetAllNeInfo(&nes) + if err != nil { + log.Error("Failed to get all ne info:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + + for _, ne := range nes { + hostUri := fmt.Sprintf("http://%s:%v", ne.Ip, ne.Port) + apiUri := fmt.Sprintf(UriAlarmsFmt, strings.ToLower(ne.NeType)) + requestURI2NF := fmt.Sprintf("%s%s", hostUri, apiUri) + log.Debug("requestURI2NF: Get ", requestURI2NF) + client := resty.New() + response, err := client.R(). + EnableTrace(). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + Get(requestURI2NF) + if err != nil { + log.Error("Failed to Get:", err) + //services.ResponseInternalServerError500ProcessError(w, err) + continue + } + body := response.Body() + log.Debug("Request body:", string(body)) + alarmArray := new([]Alarm) + err = json.Unmarshal(body, &alarmArray) + if err != nil { + log.Error("Failed to Unmarshal:", err) + //services.ResponseInternalServerError500ProcessError(w, err) + continue + } + valueJson, err := dborm.XormGetAAConfig() + if err != nil { + log.Error("Failed to XormGetAAConfig:", err) + //services.ResponseInternalServerError500ProcessError(w, err) + continue + } + if alarmArray == nil { + log.Info("Not found sync alarms, neType=%s, neId=%s", ne.NeType, ne.NeId) + //services.ResponseInternalServerError500ProcessError(w, err) + continue + } + // session := xEngine.NewSession() + // defer session.Close() + var activeAlarmNum int = 0 + for _, alarmData := range *alarmArray { + log.Debug("alarmData:", alarmData) + + session := xEngine.NewSession() + defer session.Close() + // todo: clear alarm .... + if alarmData.AlarmStatus == AlarmStatusClear { + exist, err := session.Table("alarm"). + Where("ne_type=? and ne_id=? and alarm_id=? and alarm_status=1", alarmData.NeType, alarmData.NeId, alarmData.AlarmId). + Exist() + if err == nil || exist == false { + log.Info("Not found active alarm: ne_id=%s, alarm_id=%s", alarmData.NeId, alarmData.AlarmId) + continue + } + alarmData.ClearType = ClearTypeAutoClear + alarmData.ClearTime.Valid = true + tm, _ := time.Parse(time.RFC3339, alarmData.EventTime) + log.Debugf("EventTime:%s tm:%d tm-datetime:%s", alarmData.EventTime, tm, tm.Local().Format(time.DateTime)) + alarmData.ClearTime.Time = tm + if IsNeedToAckAlarm(valueJson, &alarmData) == true { + SetAlarmAckInfo(valueJson, &alarmData) + log.Debug("alarmData:", alarmData) + affected, err := session. + Where("ne_type=? and ne_id=? and alarm_id=? and alarm_status=1", alarmData.NeType, alarmData.NeId, alarmData.AlarmId). + Cols("alarm_status", "clear_type", "clear_time", "ack_state", "ack_time", "ack_user"). + Update(alarmData) + if err != nil && affected <= 0 { + log.Error("Failed to update alarm data:", err) + //services.ResponseInternalServerError500DatabaseOperationFailed(w) + continue + } + } else { + affected, err := session. + Where("ne_type=? and ne_id=? and alarm_id=? and alarm_status=1", alarmData.NeType, alarmData.NeId, alarmData.AlarmId). + Cols("alarm_status", "clear_type", "clear_time"). + Update(alarmData) + if err != nil && affected <= 0 { + log.Error("Failed to update alarm data:", err) + //services.ResponseInternalServerError500DatabaseOperationFailed(w) + continue + } + } + + eventTime := global.GetFmtTimeString(time.RFC3339, alarmData.EventTime, time.DateTime) + alarmLog := new(AlarmLog) + alarmLog.NeType = alarmData.NeType + alarmLog.NeId = alarmData.NeId + alarmLog.AlarmSeq = alarmData.AlarmSeq + alarmLog.AlarmId = alarmData.AlarmId + alarmLog.AlarmCode = alarmData.AlarmCode + alarmLog.AlarmStatus = alarmData.AlarmStatus + alarmLog.EventTime = eventTime + log.Debug("alarmLog:", alarmLog) + + affected, err := session.Insert(alarmLog) + if err != nil && affected <= 0 { + log.Error("Failed to insert alarm_log:", err) + } + + // todo: PerceivedSeverity set color + var severity string + has, err := xEngine.Table("alarm"). + Where("ne_type=? and ne_id=? and event_time=? and alarm_status=1", alarmData.NeType, alarmData.NeId, alarmData.EventTime). + //OrderBy("FIELD(orig_severity, 'Critical', 'Major', 'Minor', 'Warning', 'Event') ASC"). + Asc("orig_severity"). + Cols("orig_severity"). + Limit(1). + Get(&severity) + if err != nil { + log.Error("Failed to get alarm:", err) + continue + } + log.Debugf("neType=%s, neId=%s, eventTime=%s, severity=%s", alarmData.NeType, alarmData.NeId, alarmData.EventTime, severity) + + if has == true && severity > alarmData.OrigSeverity { + // update exist record + _, err := session.Table("alarm"). + Where("ne_type=? and ne_id=? and event_time=?", alarmData.NeType, alarmData.NeId, alarmData.EventTime). + Update(&Alarm{PerceivedSeverity: severity}) + if err != nil { + log.Error("Failed to update alarm:", err) + continue + } + } + session.Commit() + // for alarm forward time format + alarmData.EventTime = eventTime + } else { + activeAlarmNum++ + has, err := xEngine.Table("alarm"). + Where("alarm_id=? and ne_type=? and ne_id=? and alarm_status=1", + alarmData.AlarmId, alarmData.NeType, alarmData.NeId). + Exist() + if err == nil && has == true { + log.Warn("Exist the same alarm") + continue + } + + var currentSeq string + has, err = xEngine.Table("alarm"). + Where("ne_type=? and ne_id=?", alarmData.NeType, alarmData.NeId). + Desc("alarm_seq"). + //Desc("event_time"). + Cols("alarm_seq"). + Limit(1). + Get(¤tSeq) + if err != nil { + log.Error("Failed to get alarm:", err) + continue + } + log.Debugf("neType=%s, neId=%s, currentSeq=%s, activeAlarmNum=%d", + alarmData.NeType, alarmData.NeId, currentSeq, activeAlarmNum) + + if has == true { + seq, _ := strconv.Atoi(currentSeq) + alarmData.AlarmSeq = seq + 1 + if alarmData.AlarmSeq > global.MaxInt32Number { + alarmData.AlarmSeq = AlarmSeqBeginNumber + } + } else { + alarmData.AlarmSeq = AlarmSeqBeginNumber + } + + // todo: PerceivedSeverity set color + var severity string + has, err = xEngine.Table("alarm"). + Where("ne_type=? and ne_id=? and event_time=? and alarm_status=1", alarmData.NeType, alarmData.NeId, alarmData.EventTime). + //OrderBy("FIELD(orig_severity, 'Critical', 'Major', 'Minor', 'Warning', 'Event') ASC"). + Asc("orig_severity"). + Cols("orig_severity"). + Limit(1). + Get(&severity) + if err != nil { + log.Error("Failed to get alarm:", err) + continue + } + log.Debugf("neType=%s, neId=%s, eventTime=%s, severity=%s", alarmData.NeType, alarmData.NeId, alarmData.EventTime, severity) + + if has == false || severity == alarmData.OrigSeverity { + alarmData.PerceivedSeverity = alarmData.OrigSeverity + } else if severity > alarmData.OrigSeverity { + alarmData.PerceivedSeverity = alarmData.OrigSeverity + } else { + alarmData.PerceivedSeverity = severity + // update exist record + _, err := session.Table("alarm"). + Where("ne_type=? and ne_id=? and event_time=?", alarmData.NeType, alarmData.NeId, alarmData.EventTime). + Update(&Alarm{PerceivedSeverity: alarmData.PerceivedSeverity}) + if err != nil { + log.Error("Failed to update alarm:", err) + continue + } + } + + alarmData.ObjectUid = alarmData.NeId + alarmData.ObjectType = "VNFM" + alarmData.EventTime = global.GetFmtTimeString(time.RFC3339, alarmData.EventTime, time.DateTime) + if IsNeedToAckAlarm(valueJson, &alarmData) == true { + SetAlarmAckInfo(valueJson, &alarmData) + } + log.Trace("alarmData:", alarmData) + affected, err := session.Insert(alarmData) + if err == nil && affected > 0 { + alarmLog := new(AlarmLog) + alarmLog.NeType = alarmData.NeType + alarmLog.NeId = alarmData.NeId + alarmLog.AlarmSeq = alarmData.AlarmSeq + alarmLog.AlarmId = alarmData.AlarmId + alarmLog.AlarmCode = alarmData.AlarmCode + alarmLog.AlarmStatus = alarmData.AlarmStatus + alarmLog.EventTime = global.GetFmtTimeString(time.RFC3339, alarmData.EventTime, time.DateTime) + log.Debug("alarmLog:", alarmLog) + affected, err = session.Insert(alarmLog) + if err != nil && affected <= 0 { + log.Error("Failed to insert data:", err) + //services.ResponseInternalServerError500DatabaseOperationFailed(w) + continue + } + session.Commit() + if config.GetYamlConfig().Alarm.ForwardAlarm { + if err = AlarmEmailForward(&alarmData); err != nil { + log.Error("Failed to AlarmEmailForward:", err) + } + if err = AlarmForwardBySMS(&alarmData); err != nil { + log.Error("Failed to AlarmForwardBySMS:", err) + } + } + } + log.Warn("Failed to insert alarm data:", err) + } + } + } + services.ResponseStatusOK200Null(w) +} diff --git a/features/fm/email.go b/features/fm/email.go new file mode 100644 index 0000000..79665a2 --- /dev/null +++ b/features/fm/email.go @@ -0,0 +1,111 @@ +package fm + +import ( + "crypto/tls" + "errors" + "fmt" + "strings" + + "ems.agt/lib/dborm" + "ems.agt/lib/log" + "ems.agt/restagent/config" + + "gopkg.in/gomail.v2" +) + +func AlarmEmailForward(alarmData *Alarm) error { + log.Info("AlarmEmailForward processing... ") + + message := ` +
Hello information,
+ test, test +Best Wishes!
+ ` + + // QQ 邮箱: + // SMTP 服务器地址:smtp.qq.com(SSL协议端口:465/994 | 非SSL协议端口:25) + // 163 邮箱: + // SMTP 服务器地址:smtp.163.com(端口:25) + // host := "mail.agrandtech.com" + // port := 25 + // userName := "smtpext@agrandtech.com" + // password := "1000smtp@omc!" + + host := config.GetYamlConfig().Alarm.Email.Smtp + port := int(config.GetYamlConfig().Alarm.Email.Port) + userName := config.GetYamlConfig().Alarm.Email.User + password := config.GetYamlConfig().Alarm.Email.Password + + m := gomail.NewMessage() + m.SetHeader("From", userName) // 发件人 + //m.SetHeader("From", "alias"+"<"+"aliastest"+">") // 增加发件人别名 + + emails, err := dborm.XormGetAlarmForward("Email") + if err != nil { + log.Error("Failed to XormGetAlarmForward:", err) + return err + } else if emails == nil || len(*emails) == 0 { + err := errors.New("not found forward email list") + log.Error(err) + return err + } + + forwardLog := &dborm.AlarmForwardLog{ + NeType: alarmData.NeType, + NeID: alarmData.NeId, + AlarmID: alarmData.AlarmId, + AlarmTitle: alarmData.AlarmTitle, + AlarmSeq: alarmData.AlarmSeq, + EventTime: alarmData.EventTime, + ToUser: strings.Join(*emails, ","), + } + for _, email := range *emails { + m.SetHeader("To", email) // 收件人,可以多个收件人,但必须使用相同的 SMTP 连接 + } + + //m.SetHeader("To", "zhangshuzhong@agrandtech.com", "simonzhangsz@outlook.com") // 收件人,可以多个收件人,但必须使用相同的 SMTP 连接 + //m.SetHeader("Cc", "******@qq.com") // 抄送,可以多个 + //m.SetHeader("Bcc", "******@qq.com") // 暗送,可以多个 + m.SetHeader("Subject", "Alarm from OMC!") // 邮件主题 + + // text/html 的意思是将文件的 content-type 设置为 text/html 的形式,浏览器在获取到这种文件时会自动调用html的解析器对文件进行相应的处理。 + // 可以通过 text/html 处理文本格式进行特殊处理,如换行、缩进、加粗等等 + //m.SetBody("text/html", fmt.Sprintf(message, *alarm)) + m.SetBody("text/html", message) + + // text/plain的意思是将文件设置为纯文本的形式,浏览器在获取到这种文件时并不会对其进行处理 + // m.SetBody("text/plain", "纯文本") + // m.Attach("test.sh") // 附件文件,可以是文件,照片,视频等等 + // m.Attach("lolcatVideo.mp4") // 视频 + // m.Attach("lolcat.jpg") // 照片 + + d := gomail.NewDialer( + host, + port, + userName, + password, + ) + // 关闭SSL协议认证 + d.TLSConfig = &tls.Config{InsecureSkipVerify: true} + + if err := d.DialAndSend(m); err != nil { + operResult := fmt.Sprintf("Failed to DialAndSend:%v", err) + log.Error(operResult) + forwardLog.OperResult = operResult + affected, err := dborm.XormInsertAlarmForwardLog(forwardLog) + if err != nil && affected <= 0 { + log.Error("Failed to insert data:", err) + } + return err + } + + operResult := fmt.Sprintf("Email sent successfully!:", err) + log.Error(operResult) + forwardLog.OperResult = operResult + affected, err := dborm.XormInsertAlarmForwardLog(forwardLog) + if err != nil && affected <= 0 { + log.Error("Failed to insert data:", err) + return err + } + return nil +} diff --git a/features/fm/smsforward.go b/features/fm/smsforward.go new file mode 100644 index 0000000..c58e6e5 --- /dev/null +++ b/features/fm/smsforward.go @@ -0,0 +1,103 @@ +package fm + +import ( + "errors" + "fmt" + "net/http" + "net/url" + + "ems.agt/lib/dborm" + "ems.agt/lib/log" + "ems.agt/restagent/config" +) + +func AlarmForwardBySMS(alarmData *Alarm) error { + log.Info("AlarmForwardBySMS processing... ") + + SMSFforwardconfig := config.GetYamlConfig().Alarm.SMS + // 阿里云短信API的请求地址 + apiURL := SMSFforwardconfig.ApiURL + + // 阿里云短信API的AccessKey ID和AccessKey Secret + //accessKeyID := SMSFforwardconfig.AccessKeyID + accessKeySecret := SMSFforwardconfig.AccessKeySecret + + toUsers, err := dborm.XormGetAlarmForward("SMS") + if err != nil { + log.Error("Failed to XormGetAlarmForward:", err) + return err + } else if toUsers == nil { + err := errors.New("not found forward phone number") + log.Error(err) + return err + } + + for _, toUser := range *toUsers { + // 短信相关参数 + params := url.Values{} + params.Set("PhoneNumbers", toUser) + params.Set("SignName", SMSFforwardconfig.SignName) + params.Set("TemplateCode", SMSFforwardconfig.TemplateCode) + params.Set("TemplateParam", `{"message":"alarm"}`) + + // 构建请求URL + reqURL := apiURL + "?Action=SendSms&" + params.Encode() + + // 创建HTTP请求 + req, err := http.NewRequest("GET", reqURL, nil) + if err != nil { + log.Error("Failed to create request:", err) + return err + } + + // 添加请求头部 + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Authorization", "APPCODE "+accessKeySecret) + + forwardLog := &dborm.AlarmForwardLog{ + NeType: alarmData.NeType, + NeID: alarmData.NeId, + AlarmID: alarmData.AlarmId, + AlarmTitle: alarmData.AlarmTitle, + AlarmSeq: alarmData.AlarmSeq, + EventTime: alarmData.EventTime, + ToUser: toUser, + } + // 发送请求 + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + operResult := fmt.Sprintf("Failed to send request:%v", err) + log.Error(operResult) + forwardLog.OperResult = operResult + affected, err := dborm.XormInsertAlarmForwardLog(forwardLog) + if err != nil && affected <= 0 { + log.Error("Failed to insert data:", err) + } + continue + } + defer resp.Body.Close() + + // 解析响应 + if resp.StatusCode == http.StatusOK { + log.Info("SMS sent successfully!") + operResult := fmt.Sprintf("SMS sent successfully!") + forwardLog.OperResult = operResult + affected, err := dborm.XormInsertAlarmForwardLog(forwardLog) + if err != nil && affected <= 0 { + log.Error("Failed to insert data:", err) + continue + } + } else { + operResult := fmt.Sprintf("Failed to send SMS, StatusCode=%d", resp.StatusCode) + log.Error(operResult) + forwardLog.OperResult = operResult + affected, err := dborm.XormInsertAlarmForwardLog(forwardLog) + if err != nil && affected <= 0 { + log.Error("Failed to insert data:", err) + continue + } + } + } + return nil +} diff --git a/features/handle/handle.go b/features/handle/handle.go new file mode 100644 index 0000000..5c71b00 --- /dev/null +++ b/features/handle/handle.go @@ -0,0 +1,586 @@ +package handle + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "strconv" + "strings" + + "ems.agt/lib/dborm" + + "github.com/gorilla/mux" + g "github.com/gosnmp/gosnmp" + + "ems.agt/lib/global" + "ems.agt/lib/log" + "ems.agt/lib/oauth" + "ems.agt/lib/services" + "ems.agt/lib/session" + "ems.agt/restagent/config" +) + +var TodoList []stTodo + +type stTodo struct { + No string + Item string + Value string +} + +type ErrorOAuthResponse struct { + Error map[string]interface{} +} + +type FailOAuthResponse struct { + Error struct { + ErrorCode string + ErrorInfo string + } +} + +type ApiResponse struct { + ResultCode string + ResultMessage interface{} +} + +var globalSession = session.NewSessManager("restagent") + +func init() { + conf := config.GetYamlConfig() + // Default is a pointer to a GoSNMP struct that contains sensible defaults + // eg port 161, community public, etc + g.Default.Target = conf.NE.Addr + g.Default.Port = conf.NE.Port + err := g.Default.Connect() + if err != nil { + log.Fatalf("Connect() err: %v", err) + } + //defer g.Default.Conn.Close() +} + +/* +func IsValidOAuthInfo(oAuthBody OAuthBody) bool { + log.Debug("IsValidOAuthInfo processing... ") + + conf := config.GetYamlConfig() + for _, o := range conf.Auth { + if oAuthBody.GrantType == o.Type && oAuthBody.UserName == o.User && oAuthBody.Value == o.Password { + return true + } + } + + return false +} + +func IsWrongOAuthInfo(oAuthBody OAuthBody) bool { + log.Debug("IsWrongOAuthInfo processing... ") + + if oAuthBody.GrantType == "" || strings.ToLower(oAuthBody.GrantType) != "password" || oAuthBody.UserName == "" || oAuthBody.Value == "" { + return true + } + + return false +} +*/ + +func IsValidOAuthUri(r *http.Request) bool { + vars := mux.Vars(r) + Uri := vars["apiCategory"] + "/" + vars["apiVersion"] // 获取Uri + if Uri != "securityManagement/v1" { + return false + } + + return true +} + +func IsVallidContentType(r *http.Request) bool { + log.Debug("IsVallidContentType processing ...") + + ctype := r.Header["Content-Type"] + if len(ctype) != 0 && !strings.Contains(ctype[0], "application/json") { + return false + } + + return true +} + +func LoginFromOMC(w http.ResponseWriter, r *http.Request) { + log.Debug("LoginFromOMC processing... ") + + body, err := io.ReadAll(io.LimitReader(r.Body, global.RequestBodyMaxLen)) //io.LimitReader限制大小 + if err != nil { + log.Debug(err) + services.ResponseNotFound404UriNotExist(w, r) + return + } + + // check media type(content type) only support "application/json" + /* if !IsVallidContentType(r) { + log.Debug("Invalid Content-Type") + services.ResponseUnsupportedMediaType415(w) + return + } + + // check extend uri, response 404 + if !IsValidOAuthUri(r) { + log.Debug("Uri is invalid") + services.ResponseNotFound404UriNotExist(w, r) + return + } + + */ + // Error process .... + // response 400-7 + if !json.Valid([]byte(body)) { + log.Debug("Invalid Json Format") + services.ResponseBadRequest400InvalidJson(w) + return + } + + var oAuthBody oauth.OAuthBody + _ = json.Unmarshal(body, &oAuthBody) //转为json + log.Debug("body:", string(body), "oAuthBody:", oAuthBody) + + defer r.Body.Close() + // response 400-5 + if oauth.IsWrongOAuthInfo(oAuthBody) { + log.Debug("Wrong parameter value") + services.ResponseBadRequest400WrongParamValue(w) + return + } + /* + if oauth.IsValidOAuthInfo(oAuthBody) { + plist := config.GetPermissionFromConfig(oAuthBody.UserName, oAuthBody.GrantType) + log.Debug("Permission list:", plist) + + token := globalSession.NewSession(w, r, plist) + services.ResponseStatusOK200Login(w, token) + } else { + // response 400-4 + log.Debug("Authentication failed, mismatch user or password") + + services.ResponseBadRequest400IncorrectLogin(w) + } + */ + validUser, changePassword, _ := dborm.XormCheckLoginUser(oAuthBody.UserName, + oAuthBody.Value, config.GetYamlConfig().Auth.Crypt) + if validUser { + // plist := config.GetPermissionFromConfig(oAuthBody.UserName, oAuthBody.GrantType) + plist := []bool{true, true, true, true} + log.Debug("Permission list:", plist) + + token := globalSession.NewSession(w, r, plist) + services.ResponseStatusOK200Login(w, token, changePassword) + } else { + // response 400-4 + log.Error("Authentication failed, mismatch user or password") + + services.ResponseBadRequest400IncorrectLogin(w) + } + return +} + +func LogoutFromOMC(w http.ResponseWriter, r *http.Request) { + log.Debug("LogoutFromOMC processing... ") + + // check media type(content type) only support "application/json" + if !IsVallidContentType(r) { + log.Debug("Invalid Content-Type") + services.ResponseUnsupportedMediaType415(w) + return + } + + // check extend uri, response 404 + if !IsValidOAuthUri(r) { + log.Debug("Uri is invalid") + services.ResponseNotFound404UriNotExist(w, r) + return + } + + // error processing ... + // 401-1 response + token, ret := globalSession.IsCarriedToken(r) + if ret == false { + log.Debug("AccessToken is not carried") + services.ResponseUnauthorized401AccessTokenNotCarried(w) + return + } + + // 401-2 response + if globalSession.IsValidToken(token) == false { + log.Debug("AccessToken fails or does not exist") + services.ResponseUnauthorized401AccessTokenNotExist(w) + return + } + + globalSession.EndSession(w, r) + services.ResponseStatusOK200Null(w) + return +} + +func HandshakeFromOMC(w http.ResponseWriter, r *http.Request) { + log.Debug("HandshakeFromOMC processing... ") + + // check media type(content type) only support "application/json" + if !IsVallidContentType(r) { + log.Debug("Invalid Content-Type") + services.ResponseUnsupportedMediaType415(w) + return + } + + // check extend uri, response 404 + if !IsValidOAuthUri(r) { + log.Debug("Uri is invalid") + services.ResponseNotFound404UriNotExist(w, r) + return + } + + // error processing ... + // 401-1 response + token, ret := globalSession.IsCarriedToken(r) + if ret == false { + log.Debug("AccessToken is not carried") + services.ResponseUnauthorized401AccessTokenNotCarried(w) + return + } + + if !globalSession.ShakeSession(token) { + // 401-2 response + log.Debug("AccessToken fails or does not exist") + services.ResponseUnauthorized401AccessTokenNotExist(w) + return + } + + // 200 response + services.ResponseStatusOK200Null(w) + return +} + +var ( + MAX_RMUID_NUM = config.GetRmUIDMaxNumFromConfig() + MAX_ALARMID_NUM = config.GetAlarmIDMaxNumFromConfig() + MAX_PMUID_NUM = config.GetPmIDMaxNumFromConfig() + MAX_SUBID_NUM = config.GetSubIDMaxNumFromConfig() + MAX_URI_LEN = config.GetUriMaxLenFromConfig() + RMUID_REGEXP = config.GetRmUIDRegexpFromConfig() +) + +func CheckParameterName(r *http.Request) []string { + var errorParams []string + vars := r.URL.Query() + for k, v := range vars { + log.Debug("vars:", k, v) + if k != "rmUIDs" && k != "fields" { + errorParams = append(errorParams, k) + } + } + + return errorParams +} + +func GetRmUIDArr(r *http.Request) []string { + vars := r.URL.Query() + rmUIDs, ok := vars["rmUIDs"] + if !ok { + log.Debug("rmUIDs is not exist") + return nil + } + + var rmUIDValues []string + for _, r := range rmUIDs { + if r != "" { + rmUIDValues = global.MergeStringArr(rmUIDValues, strings.Split(r, `,`)) + } + } + + return rmUIDValues +} + +func GetAttrNameArr(r *http.Request) []string { + vars := r.URL.Query() + fields, ok := vars["fields"] + if !ok { + log.Debug("attributeNames does not exist") + return nil + } + var attrNames []string + for _, a := range fields { + if a != "" { + attrNames = global.MergeStringArr(attrNames, strings.Split(a, `,`)) + } + } + + return attrNames +} + +func CheckValidRmUID(rmUIDs []string) []string { + log.Debug("GetLocalRmUID processing... ") + + var invalidRmUIDs []string + for _, r := range rmUIDs { + if !global.MatchRmUID(RMUID_REGEXP, r) { + invalidRmUIDs = append(invalidRmUIDs, r) + } + } + + return invalidRmUIDs +} + +func CheckLocalRmUID(rmUIDs []string) string { + log.Debug("GetLocalRmUID processing... ") + + rmUID := config.GetRmUIDFromConfig() + for _, r := range rmUIDs { + if r == rmUID { + return rmUID + } + } + + return "" +} + +func GetNRMByUri(w http.ResponseWriter, r *http.Request) { + log.Debug("GetNRMByUri processing... ") + + // response 414-4 uri too long ? (optional) + // todo ... ? + if bytes.Count([]byte(r.RequestURI), nil) > MAX_URI_LEN { + log.Debug("Request Uri too long:", bytes.Count([]byte(r.RequestURI), nil)) + services.ResponseRequestURITooLong414UriTooLong(w) + return + } + + // check media type(content type) only support "application/json" + // response 415-1 + if !IsVallidContentType(r) { + log.Debug("Invalid Content-Type") + services.ResponseUnsupportedMediaType415(w) + return + } + + // error processing ... + // 401-1 response + token, ret := globalSession.IsCarriedToken(r) + if ret == false { + log.Debug("AccessToken is not carried") + services.ResponseUnauthorized401AccessTokenNotCarried(w) + return + } + + // 401-2 response + if globalSession.IsValidToken(token) == false { + log.Debug("AccessToken fails or does not exist") + services.ResponseUnauthorized401AccessTokenNotExist(w) + return + } + // response 403 Forbidden, permissions deny + // todo... + plist := globalSession.GetPermissionFromSession(token) + log.Debug("permission list:", plist) + if len(plist) == 0 || plist[0] == false { + log.Debug("User permission deny") + services.ResponseForbidden403NotPermission(w) + return + } + + vars := mux.Vars(r) + qeuryUri := vars["apiCategory"] + "/" + vars["elementTypeValue"] + "/" + vars["objectTypeValue"] + log.Debug("Get by Uri: ", qeuryUri) + + apiVer := vars["apiVersion"] + if apiVer != "v1" { + log.Debug("Uri is invalid") + services.ResponseNotFound404UriNotExist(w, r) + return + } + + // response 406-1 + rmUIDValues := GetRmUIDArr(r) + if rmUIDValues == nil { + log.Debug("missing parameter: rmUIDs") + services.ResponseNotAcceptable406MissingParam(w) + return + } + + // response 406-2 + errorParams := CheckParameterName(r) + if errorParams != nil { + log.Debug("parameter name error: ", errorParams) + services.ResponseNotAcceptable406ParamError(w, errorParams) + return + } + + // response 400-5 + if len(rmUIDValues) == 0 { + log.Debug("rmUIDs is wrong or NULL") + services.ResponseBadRequest400WrongParamValue(w) + return + } + + // response 414-1 + if len(rmUIDValues) > MAX_RMUID_NUM { + log.Debug("rmUID greater than", MAX_RMUID_NUM) + services.ResponseRequestURITooLong414NRMNumExceed(w, MAX_RMUID_NUM) + return + } + + // response 400-1 + // check rmUID is valid + // todo ... + invalidRmUIDs := CheckValidRmUID(rmUIDValues) + if len(invalidRmUIDs) != 0 { + log.Debug("rmUID is invalid") + services.ResponseBadRequest400RmUIDsIsInvalid(w, invalidRmUIDs) + return + } + + // response 404-2 + rmUID := CheckLocalRmUID(rmUIDValues) + if rmUID == "" { + log.Debug("rmUID does not exist") + services.ResponseNotFound404NRMNotExist(w, rmUIDValues) + return + } + + // response 404-1, uri is not exist in map + attrNames := GetAttrNameArr(r) + var Oids []string + Oids = *config.GetOidByFileds(qeuryUri, attrNames, &Oids) + if len(Oids) == 0 { + log.Debug("Nothing of config map") + services.ResponseNotFound404UriNotExist(w, r) + return + } + + // response 404-1, uri is not exist in map + var nameOids []config.NameOid + nameOids = *config.GetDataOidByFields(qeuryUri, attrNames, &nameOids) + if len(nameOids) == 0 { + log.Debug("Nothing of config map") + services.ResponseNotFound404UriNotExist(w, r) + return + } + + result, err2 := g.Default.Get(Oids) // Get() accepts up to g.MAX_OIDS + if err2 != nil { + log.Fatalf("Get() err: %v", err2) + + } + + // var nameValues []config.NameValue + var nameValue config.NameValue + + nameValues := make(map[string]interface{}) + nameValues["rmUID"] = rmUID + for i, variable := range result.Variables { + nameValue.Name = nameOids[i].Name + log.Debugf("%d: oid: %s name: %s\n", i, variable.Name, nameValue.Name) + // if nameOids[i].Oid == variable.Name && global.IsContain(attributeNames, nameValue.Name) { + if nameOids[i].Oid == variable.Name { + // the Value of each variable returned by Get() implements + // interface{}. You could do a type switch... + switch variable.Type { + case g.OctetString: + bytes := variable.Value.([]byte) + log.Debugf("string: %s\n", string(bytes)) + nameValue.Value = string(bytes) + nameValues[nameValue.Name] = nameValue.Value + case g.Integer: + value := variable.Value.(int) + log.Debugf("integer: %d\n", value) + nameValue.Value = strconv.Itoa(value) + nameValues[nameValue.Name] = nameValue.Value + case g.IPAddress: + value := variable.Value.(string) + log.Debugf("IPAddress: %s\n", variable.Value) + nameValue.Value = value + nameValues[nameValue.Name] = nameValue.Value + default: + // ... or often you're just interested in numeric values. + // ToBigInt() will return the Value as a BigInt, for plugging + // into your calculations. + log.Debugf("number: %d\n", g.ToBigInt(variable.Value)) + } + } + } + + getResponse := services.DataResponse{nameValues} + services.ResponseWithJson(w, http.StatusOK, getResponse) +} + +func RestfulPut(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + queryNo := vars["no"] + var targetTodo stTodo + for _, Todo := range TodoList { + if Todo.No == queryNo { + targetTodo = Todo + } + } + response := ApiResponse{"200", targetTodo} + services.ResponseWithJson(w, http.StatusOK, response) +} + +func RestfulHead(w http.ResponseWriter, r *http.Request) { + var targetTodo stTodo + + response := ApiResponse{"200", targetTodo} + services.ResponseWithJson(w, http.StatusOK, response) + +} + +func RemoveElement(TodoList []stTodo, No string) stTodo { + // var targetTodo stTodo + j := 0 + if len(TodoList) == 0 { + return TodoList[j] + } + + for i := 0; i < len(TodoList); i++ { + if TodoList[i].No != No { + if i != j { + TodoList[i], TodoList[j] = TodoList[j], TodoList[i] + } + j++ + } + } + log.Debug(TodoList[j].No, TodoList[j].Item) + return TodoList[j] +} + +func RestfulDelete(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + queryNo := vars["no"] //获取No + + log.Debug("RestfulDelete processing... ") + + targetTodo := RemoveElement(TodoList, queryNo) + response := ApiResponse{"200", targetTodo} + services.ResponseWithJson(w, http.StatusOK, response) +} + +func RestfulOptions(w http.ResponseWriter, r *http.Request) { + var targetTodo stTodo + + response := ApiResponse{"200", targetTodo} + services.ResponseWithJson(w, http.StatusOK, response) + +} + +func RestfulTrace(w http.ResponseWriter, r *http.Request) { + var targetTodo stTodo + + response := ApiResponse{"200", targetTodo} + services.ResponseWithJson(w, http.StatusOK, response) + +} + +func RestfulPatch(w http.ResponseWriter, r *http.Request) { + var targetTodo stTodo + + response := ApiResponse{"200", targetTodo} + services.ResponseWithJson(w, http.StatusOK, response) + +} diff --git a/features/lm/logbak.go b/features/lm/logbak.go new file mode 100644 index 0000000..0cca290 --- /dev/null +++ b/features/lm/logbak.go @@ -0,0 +1,149 @@ +package lm + +import ( + "fmt" + "net/http" + "os/exec" + "time" + + "ems.agt/lib/global" + "ems.agt/lib/log" + "ems.agt/lib/services" + "ems.agt/restagent/config" + + "github.com/gorilla/mux" + "xorm.io/xorm" +) + +type XormResponse struct { + Data interface{} `json:"data"` +} + +type XormInsertResponse struct { + Data interface{} `json:"data"` +} + +var ( + ExtBackupDataUri = config.DefaultUriPrefix + "/dataManagement/{apiVersion}/{dataStorage}/{dataObject}/backup" // for external + + CustomExtBackupDataUri = config.UriPrefix + "/dataManagement/{apiVersion}/{dataStorage}/{dataObject}/backup" // for external +) + +var XEngine *xorm.Engine + +type DatabaseClient struct { + dbType string + dbUrl string + dbConnMaxLifetime time.Duration + dbMaxIdleConns int + dbMaxOpenConns int + IsShowSQL bool + + XEngine *xorm.Engine +} + +var DbClient DatabaseClient + +// func init() { +// conf := config.GetYamlConfig() +// InitDbClient(conf.Database.Type, conf.Database.User, conf.Database.Password, +// conf.Database.Host, conf.Database.Port, conf.Database.Name) +// } + +func InitDbClient(dbType, dbUser, dbPassword, dbHost, dbPort, dbName string) error { + DbClient.dbUrl = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=true&loc=Local", + dbUser, dbPassword, dbHost, dbPort, dbName) + DbClient.dbType = dbType + DbClient.dbConnMaxLifetime = 0 + DbClient.dbMaxIdleConns = 0 + DbClient.dbMaxOpenConns = 0 + if log.GetLevel() == log.LOG_TRACE { + DbClient.IsShowSQL = true + } + log.Debugf("dbType:%s dbUrl:%s:******@tcp(%s:%s)/%s??charset=utf8&parseTime=true&loc=Local", + dbType, dbUser, dbHost, dbPort, dbName) + + var err error + DbClient.XEngine, err = xorm.NewEngine(DbClient.dbType, DbClient.dbUrl) + if err != nil { + log.Error("Failed to connet database:", err) + return err + } + DbClient.XEngine.SetConnMaxLifetime(DbClient.dbConnMaxLifetime) + DbClient.XEngine.SetMaxIdleConns(DbClient.dbMaxIdleConns) + DbClient.XEngine.SetMaxOpenConns(DbClient.dbMaxOpenConns) + if DbClient.IsShowSQL { + DbClient.XEngine.ShowSQL(true) + } + XEngine = DbClient.XEngine + + return nil +} + +func ExtDatabaseBackupData(w http.ResponseWriter, r *http.Request) { + log.Debug("ExtDatabaseBackupData processing... ") + + token, err := services.CheckExtValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + vars := mux.Vars(r) + module := vars["managementModule"] + dbname := vars["dataStorage"] + tbname := vars["dataObject"] + pack := "lm" + log.Debugf("token:%s, method:%s, module:%s dbname:%s, tbname:%s pack:%s", token, r.Method, module, dbname, tbname, pack) + // exist, err := services.CheckUserPermission(token, strings.ToLower(r.Method), module, dbname, tbname, pack) + // if err != nil { + // log.Error("Failed to get permission:", err) + // services.ResponseForbidden403NotPermission(w) + // return + // } + // if !exist { + // log.Error("permission deny!") + // services.ResponseForbidden403NotPermission(w) + // return + // } + + var sql string + var filePath string + switch tbname { + case "operation_log": + filePath = fmt.Sprintf("/tmp/%s-%s.csv", tbname, time.Now().Local().Format(global.DateData)) + sql = fmt.Sprintf("select * into outfile '%s' fields terminated by ',' escaped by '' optionally enclosed by '' lines terminated by '\n' from (select 'op_id','account_name','op_ip','subsys_tag','op_type','op_content','op_result','begin_time','end_time','vnf_flag','log_time' union select op_id,account_name,op_ip,subsys_tag,op_type,op_content,op_result,begin_time,end_time,vnf_flag,log_time from operation_log) b", filePath) + case "security_log": + filePath = fmt.Sprintf("/tmp/%s-%s.csv", tbname, time.Now().Local().Format(global.DateData)) + sql = fmt.Sprintf("select * into outfile '%s' fields terminated by ',' escaped by '' optionally enclosed by '' lines terminated by '\n' from (select 'id','account_name','account_type','op_ip','op_type','op_content','op_result','op_time' union select id,account_name,account_type,op_ip,op_type,op_content,op_result,op_time from security_log) b", filePath) + case "alarm_log": + filePath = fmt.Sprintf("/tmp/%s-%s.csv", tbname, time.Now().Local().Format(global.DateData)) + sql = fmt.Sprintf("select * into outfile '%s' fields terminated by ',' escaped by '' optionally enclosed by '' lines terminated by '\n' from (select 'id','ne_type','ne_id','alarm_seq','alarm_id','alarm_code','alarm_status','event_time','log_time' union select id,ne_type,ne_id,alarm_seq,alarm_id,alarm_code,alarm_status,event_time,log_time from alarm_log) b", filePath) + default: + log.Error("error target table") + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + + res, err := DbClient.XEngine.Exec(sql) + if err != nil { + log.Error("Failed to exec SQL:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + affected, _ := res.RowsAffected() + + log.Debugf("filePath:%s backup dir:%s", filePath, config.GetYamlConfig().Database.Backup) + cmd := exec.Command("cp", "-rf", filePath, config.GetYamlConfig().Database.Backup) + out, err := cmd.CombinedOutput() + log.Debugf("Exec output: %v", string(out)) + if err != nil { + log.Error("Faile to exec:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + mapRow := make(map[string]interface{}) + row := map[string]interface{}{"affectedRows": affected} + mapRow[tbname] = row + services.ResponseWithJson(w, http.StatusOK, mapRow) +} diff --git a/features/maintenance/maintenance.go b/features/maintenance/maintenance.go new file mode 100644 index 0000000..e297671 --- /dev/null +++ b/features/maintenance/maintenance.go @@ -0,0 +1,313 @@ +package maintenance + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "os/exec" + "path" + "runtime" + "time" + + "ems.agt/lib/core/utils/ctx" + "ems.agt/lib/dborm" + "ems.agt/lib/log" + "ems.agt/lib/services" + "ems.agt/restagent/config" + "github.com/shirou/gopsutil/cpu" + "github.com/shirou/gopsutil/disk" + "github.com/shirou/gopsutil/mem" +) + +// (1) OMC能够对相关的文件系统资源、内存、CPU资源、数据存储空间、数据库空间等系统指标进行监控管理; +// 对于虚拟化部署OMC系统,能够对虚机内存、虚机CPU、虚拟存储空间、文件系统资源、数据库空间等指标进行监控,提供界面截图 ; + +// (2) 系统监控指标的采样时间和阈值可由用户设定,超过阈值将产生不同级别的告警,提供界面截图 ; + +// (3) OMC能够方便的查询数据库连接情况;并可手工干预数据库的连接,能方便的终结非法的数据库连接 ; + +// (4) 用户能方便的查询系统进程、应用进程等的进程名、进程类型、开始时间、运行主机等信息,提供界面截图 +// (5) 用户能方便的对系统进程、应用进程等做中断或者启动操作,提供界面截图 + +// (6) 对于文件系统资源、内存、CPU资源、数据存储空间、数据库空间等系统指标数据,要求OMC能够保存至少3个月,提供界面截图 ; +// (7) 用户可以按照需求自定义报表模板并生成OMC系统维护数据报表,提供界面截图 ; + +// (8) OMC具备自身告警管理功能,对于传统OMC系统,如:服务器单电源告警,存储硬盘告警、OMC系统软件故障等; +// 对于虚拟化OMC系统,如虚机告警、虚拟硬盘告警等,提供界面截图 。 + +var ( + // parameter config management + Uri = config.UriPrefix + "/maintenance/{apiVersion}/zz" + + // (1) OMC能够对相关的文件系统资源、内存、CPU资源、数据存储空间、数据库空间等系统指标进行监控管理; + UriPref = config.UriPrefix + "/maintenance/{apiVersion}/pref" + + // (6) 对于文件系统资源、内存、CPU资源、数据存储空间、数据库空间等系统指标数据,要求OMC能够保存至少3个月,提供界面截图 ; + UriPrefLog = config.UriPrefix + "/maintenance/{apiVersion}/prefLog" + + // (2) 系统监控指标的采样时间和阈值可由用户设定,超过阈值将产生不同级别的告警,提供界面截图 ; + UriConfig = config.UriPrefix + "/maintenance/{apiVersion}/config" + + // (3) OMC能够方便的查询数据库连接情况;并可手工干预数据库的连接,能方便的终结非法的数据库连接 + UriSqlClient = config.UriPrefix + "/maintenance/{apiVersion}/sqlClient" + + // (4) 用户能方便的查询系统进程、应用进程等的进程名、进程类型、开始时间、运行主机等信息,提供界面截图 + // (5) 用户能方便的对系统进程、应用进程等做中断或者启动操作,提供界面截图 + UriTop = config.UriPrefix + "/maintenance/{apiVersion}/top" +) + +func init() { + // 定時收集 TODO + prefLogSave("") +} + +func List(w http.ResponseWriter, r *http.Request) { + fmt.Println("zz List") + services.ResponseStatusOK200Null(w) +} + +// 性能指標 +func prefInfo(dirName string) map[string]any { + data := make(map[string]any) + + // 显示文件資源目录 + dirPath := "D://" + if runtime.GOOS == "linux" { + dirPath = "/home" + } + // 訪問下級 + if dirName != "" { + dirPath = path.Join(dirPath, dirName) + } + dir_list, e := os.ReadDir(dirPath) + if e != nil { + log.Error(e) + } + list := make([]map[string]any, 0) + for _, v := range dir_list { + o, err := v.Info() + if err != nil { + continue + } + list = append(list, map[string]any{ + "name": o.Name(), + "size": o.Size(), + "mode": o.Mode().String(), + "modTime": o.ModTime().Format("2006-01-02 15:04:05"), + "isDir": o.IsDir(), + }) + } + data["dirList"] = list + + // 文件資源使用率 + u, _ := disk.Usage(dirPath) + usedGB := int(u.Used) / (1024 * 1024 * 1024 * 1) + data["dirUse"] = fmt.Sprintf("%d", usedGB) + + // CPU使用率 + percent, err := cpu.Percent(time.Second, false) + if err != nil { + log.Error(err) + } + data["cpuUse"] = fmt.Sprintf("%.2f", percent[0]) + + // 内存使用率 + memInfo, err := mem.VirtualMemory() + if err != nil { + log.Error(err) + } + data["memUse"] = memInfo.UsedPercent + + // 獲取數據庫占用空間 + if dborm.DbClient.XEngine != nil { + conf := config.GetYamlConfig() + result, err := dborm.DbClient.XEngine.QueryString(`SELECT + CONCAT(TRUNCATE(SUM(data_length)/1024/1024,2),'MB') AS data_size, + CONCAT(TRUNCATE(SUM(max_data_length)/1024/1024,2),'MB') AS max_data_size, + CONCAT(TRUNCATE(SUM(data_free)/1024/1024,2),'MB') AS data_free, + CONCAT(TRUNCATE(SUM(index_length)/1024/1024,2),'MB') AS index_size + FROM information_schema.tables WHERE TABLE_SCHEMA = ?; + `, conf.Database.Name) + if err == nil { + data["dbInfo"] = result[0] + } else { + data["dbInfo"] = map[string]string{} + } + } + return data +} + +// 性能指標存入數據庫 +func prefLogSave(dirName string) { + if dborm.DbClient.XEngine != nil { + data := prefInfo(dirName) + + dirListByte, err := json.Marshal(data["dirList"]) + if err != nil { + log.Error(err) + } + dbInfoByte, err := json.Marshal(data["dbInfo"]) + if err != nil { + log.Error(err) + } + rse, err := dborm.DbClient.XEngine.Exec(`INSERT INTO sys_perf_data + (id, create_time, dir_used, dir_list, db_info, mem_used, cpu_used) + VALUES(NULL, NOW(), ?, ?, ?, ?, ?); + `, data["dirUse"], string(dirListByte), string(dbInfoByte), data["memUse"], data["cpuUse"]) + if err != nil { + log.Error(err) + } + fmt.Println(rse.LastInsertId()) + } +} + +// GET http://192.168.21.183:3040/api/rest/maintenance/v1/pref?dir=true +func Pref(w http.ResponseWriter, r *http.Request) { + // 知道下級文件資源目录 + dirName := r.URL.Query().Get("dirName") + data := prefInfo(dirName) + services.ResponseWithJson(w, http.StatusOK, data) +} + +// POST http://192.168.21.183:3040/api/rest/maintenance/v1/config +func Config(w http.ResponseWriter, r *http.Request) { + // json 請求參數獲取 + var bodyArgs struct { + Key string `json:"key"` + Value string `json:"value"` + } + err := ctx.ShouldBindJSON(r, &bodyArgs) + if err != nil { + log.Error("io.ReadAll is failed:", err) + services.ResponseNotFound404UriNotExist(w, r) + return + } + + // 進行值更新 + if dborm.DbClient.XEngine != nil { + result, err := dborm.DbClient.XEngine.QueryString("SELECT * FROM information_schema.processlist") + if err != nil { + fmt.Println(err) + } + fmt.Println(result) + rse, err := dborm.DbClient.XEngine.Exec("UPDATE sys_config SET value = ? where id = ?", "true", 100) + if err != nil { + fmt.Println(err) + } + fmt.Println(rse) + } + + services.ResponseStatusOK200Null(w) +} + +// http://192.168.21.183:3040/api/rest/maintenance/v1/sqlClient?type=close +// http://192.168.21.183:3040/api/rest/maintenance/v1/sqlClient?type=connet +// http://192.168.21.183:3040/api/rest/maintenance/v1/sqlClient?type=user +func SqlClient(w http.ResponseWriter, r *http.Request) { + // 关闭 + isClose := r.URL.Query().Get("type") + if isClose == "close" && dborm.DbClient.XEngine != nil { + dborm.DbClient.XEngine.Close() + } + + // 重连 + isConnet := r.URL.Query().Get("type") + if isConnet == "connet" && dborm.DbClient.XEngine == nil { + conf := config.GetYamlConfig() + err := dborm.InitDbClient(conf.Database.Type, conf.Database.User, conf.Database.Password, + conf.Database.Host, conf.Database.Port, conf.Database.Name) + if err != nil { + fmt.Println("dborm.initDbClient err:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + } + + // 查询实例 + isUser := r.URL.Query().Get("type") + if isUser == "user" && dborm.DbClient.XEngine != nil { + result, err := dborm.DbClient.XEngine.QueryString("SELECT * FROM information_schema.processlist") + if err != nil { + fmt.Println(err) + } + fmt.Println(result) + rse, err := dborm.DbClient.XEngine.Exec("KILL CONNECTION CONNECTION_ID()") + if err != nil { + fmt.Println(err) + } + fmt.Println(rse) + } + + // 进行连接测试 + err := dborm.DbClient.XEngine.Ping() + if err != nil { + fmt.Println(err) + } + services.ResponseStatusOK200Null(w) +} + +// GET http://192.168.21.183:3040/api/rest/maintenance/v1/top?grep= +func Top(w http.ResponseWriter, r *http.Request) { + // 過濾命令 + grep := r.URL.Query().Get("grep") + // 命令拼接 + var cmd *exec.Cmd + switch runtime.GOOS { + case "linux": + command := "ps -ef " + if grep != "" { + command += grep + } + cmd = exec.Command(command) + case "windows": + command := "wmic process list brief " + if grep != "" { + command += grep + } + cmd = exec.Command("cmd", "/C", command) + } + + out, err := cmd.CombinedOutput() + fmt.Println(string(out)) + if err != nil { + fmt.Println(err) + } + services.ResponseWithJson(w, http.StatusOK, string(out)) +} + +// PATCH http://192.168.21.183:3040/api/rest/maintenance/v1/top?ops=&name= +func TopOps(w http.ResponseWriter, r *http.Request) { + // json 請求參數獲取 + var bodyArgs struct { + Ops string `json:"ops"` + Pid string `json:"pid"` + } + err := ctx.ShouldBindJSON(r, &bodyArgs) + if err != nil { + log.Error("io.ReadAll is failed:", err) + services.ResponseNotFound404UriNotExist(w, r) + return + } + // 命令拼接 + var cmd *exec.Cmd + switch runtime.GOOS { + case "linux": + switch bodyArgs.Ops { + case "kill": + cmd = exec.Command("kill", "-9", bodyArgs.Pid) + } + case "windows": + switch bodyArgs.Ops { + case "kill": + cmd = exec.Command("cmd", "/C", "taskkill", "-PID", bodyArgs.Pid, "-F") + } + } + + out, err := cmd.CombinedOutput() + fmt.Println(string(out)) + if err != nil { + fmt.Println(err) + } + services.ResponseWithJson(w, http.StatusOK, string(out)) +} diff --git a/features/mml/mml.go b/features/mml/mml.go new file mode 100644 index 0000000..045d055 --- /dev/null +++ b/features/mml/mml.go @@ -0,0 +1,246 @@ +package mml + +import ( + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "strings" + "time" + + "ems.agt/lib/dborm" + "ems.agt/lib/global" + "ems.agt/lib/log" + "ems.agt/lib/mmlp" + "ems.agt/lib/services" + "ems.agt/restagent/config" + + "github.com/gorilla/mux" + + _ "github.com/go-sql-driver/mysql" +) + +const ( + //经过测试,linux下,延时需要大于100ms + TIME_DELAY_AFTER_WRITE = 200 +) + +type Response struct { + Data []string `json:"data"` +} + +type MMLRequest struct { + MML []string `json:"mml"` +} + +var ( + // MML interface + UriMML = config.DefaultUriPrefix + "/operationManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/mml" + UriMMLDiscard = config.DefaultUriPrefix + "/opeartionManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/mml" + UriNeOmMml = config.DefaultUriPrefix + "/omManagement/{apiVersion}/mml/{netype}/{neid}" + UriOmMmlExt = config.DefaultUriPrefix + "/{managedType}/{apiVersion}/elementType/OMC/objectType/mml" + UriOmMmlInt = config.DefaultUriPrefix + "/omManagement/{apiVersion}/mml/{neType}/{neId}" + + CustomUriMML = config.UriPrefix + "/operationManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/mml" + CustomUriNeOmMml = config.UriPrefix + "/omManagement/{apiVersion}/mml/{netype}/{neid}" + CustomUriOmMmlExt = config.UriPrefix + "/opeartionManagement/{apiVersion}/elementType/OMC/objectType/mml" + CustomUriOmMmlInt = config.UriPrefix + "/omManagement/{apiVersion}/mml/{neType}/{neId}" +) + +func PostMMLToNF(w http.ResponseWriter, r *http.Request) { + log.Debug("PostMMLToNF processing... ") + + token, err := services.CheckExtValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + pack := "mml" + vars := mux.Vars(r) + module := vars["managedType"] + neType := vars["elementTypeValue"] + params := r.URL.Query() + neId := params["ne_id"] + if len(neId) == 0 { + log.Error("NOT FOUND ne_id") + services.ResponseBadRequest400WrongParamValue(w) + return + } + log.Debug("neType:", neType, "neId", neId) + + log.Debugf("token:%s, method:%s, managementType:%s dbname:%s, tbname:%s pack:%s", + token, r.Method, module, neType, neId[0], pack) + + var buf [8192]byte + var n int + var mmlResult []string + + // exist, err := services.CheckUserPermission(token, strings.ToLower(r.Method), module, neType, neId[0], pack) + // if err != nil { + // log.Error("Failed to get permission:", err) + // errMsg := fmt.Sprintf("RetCode = -1 operation failed: do not have the operation permissions") + // log.Error(errMsg) + // mmlResult = append(mmlResult, errMsg) + // response := Response{mmlResult} + // services.ResponseWithJson(w, http.StatusOK, response) + // //services.ResponseForbidden403NotPermission(w) + // return + // } + // if !exist { + // log.Error("Not permission!") + // errMsg := fmt.Sprintf("RetCode = -1 operation failed: do not have the operation permissions") + // log.Error(errMsg) + // mmlResult = append(mmlResult, errMsg) + // response := Response{mmlResult} + // services.ResponseWithJson(w, http.StatusOK, response) + // //services.ResponseForbidden403NotPermission(w) + // return + // } + + if strings.ToLower(neType) == "omc" { + PostMMLToOMC(w, r) + return + } + + neInfo := new(dborm.NeInfo) + neInfo, err = dborm.XormGetNeInfo(neType, neId[0]) + if err != nil { + log.Error("dborm.XormGetNeInfo is failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + + if neInfo != nil { + hostMML := fmt.Sprintf("%s:%d", neInfo.Ip, config.GetYamlConfig().MML.Port) + conn, err := net.Dial("tcp", hostMML) + if err != nil { + errMsg := fmt.Sprintf("Failed to dial %s: %v", hostMML, err) + log.Error(errMsg) + mmlResult = append(mmlResult, errMsg) + response := Response{mmlResult} + services.ResponseWithJson(w, http.StatusOK, response) + return + } + + loginStr := fmt.Sprintf("%s\n%s\n", config.GetYamlConfig().MML.User, config.GetYamlConfig().MML.Password) + n, err = conn.Write([]byte(loginStr)) + if err != nil { + log.Errorf("Error: %s", err.Error()) + return + } + + time.Sleep(time.Millisecond * TIME_DELAY_AFTER_WRITE) + + n, err = conn.Read(buf[0:]) + if err != nil { + log.Errorf("Error: %s", err.Error()) + return + } + log.Debug(string(buf[0:n])) + + body, err := io.ReadAll(io.LimitReader(r.Body, global.RequestBodyMaxLen)) + if err != nil { + log.Error("io.ReadAll is failed:", err) + services.ResponseNotFound404UriNotExist(w, r) + return + } + log.Debug("Body:", string(body)) + + mmlRequest := new(MMLRequest) + _ = json.Unmarshal(body, mmlRequest) + + for _, mml := range mmlRequest.MML { + mmlCommand := fmt.Sprintf("%s\n", mml) + log.Debug("mml command:", mmlCommand) + n, err = conn.Write([]byte(mmlCommand)) + if err != nil { + log.Errorf("Error: %s", err.Error()) + return + } + time.Sleep(time.Millisecond * TIME_DELAY_AFTER_WRITE) + + n, err = conn.Read(buf[0:]) + if err != nil { + log.Errorf("Error: %s", err.Error()) + return + } + log.Debug(string(buf[0 : n-len(neType)-2])) + mmlResult = append(mmlResult, string(buf[0:n-len(neType)-2])) + } + } + + response := Response{mmlResult} + services.ResponseWithJson(w, http.StatusOK, response) +} + +func PostMMLToOMC(w http.ResponseWriter, r *http.Request) { + log.Debug("PostMMLToOMC processing... ") + + token, err := services.CheckExtValidRequest(w, r) + if err != nil { + log.Error("Failed to CheckMmlValidRequest:", err) + return + } + + params := r.URL.Query() + neId := params["ne_id"] + if len(neId) == 0 { + log.Error("NOT FOUND ne_id ") + services.ResponseBadRequest400WrongParamValue(w) + return + } + + neInfo := new(dborm.NeInfo) + neInfo, err = dborm.XormGetNeInfo("OMC", neId[0]) + if err != nil { + log.Error("dborm.XormGetNeInfo is failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + log.Trace("neInfo:", neInfo) + + body, err := io.ReadAll(io.LimitReader(r.Body, global.RequestBodyMaxLen)) + if err != nil { + log.Error("io.ReadAll is failed:", err) + services.ResponseNotFound404UriNotExist(w, r) + return + } + log.Debug("Body:", string(body)) + + hostUri := fmt.Sprintf("http://%s:%s", neInfo.Ip, neInfo.Port) + + omcMmlVar := &mmlp.MmlVar{ + Version: "16.1.1", + Output: mmlp.DefaultFormatType, + MmlHome: config.GetYamlConfig().MML.MmlHome, + Limit: 50, + User: "", + SessionToken: token, + HttpUri: hostUri, + UserAgent: config.GetDefaultUserAgent(), + } + mmlRequest := new(MMLRequest) + _ = json.Unmarshal(body, mmlRequest) + + var mmlResult []string + mmlLine := strings.Join(mmlRequest.MML, ";") + + var mmlCmds []mmlp.MmlCommand + if err = mmlp.ParseMMLCommand(mmlLine, &mmlCmds); err != nil { + response := fmt.Sprintf("parse command error: %v\n", err) + mmlResult = append(mmlResult, response) + } + + for _, mmlCmd := range mmlCmds { + output, err := mmlp.TransMml2HttpReq(omcMmlVar, &mmlCmd) + if err != nil { + response := fmt.Sprintf("translate MML command error: %v]\n", err) + mmlResult = append(mmlResult, response) + } + mmlResult = append(mmlResult, string(*output)) + } + + response := Response{mmlResult} + services.ResponseWithJson(w, http.StatusOK, response) +} diff --git a/features/monitor/monitor/model.go b/features/monitor/monitor/model.go new file mode 100644 index 0000000..8997a22 --- /dev/null +++ b/features/monitor/monitor/model.go @@ -0,0 +1,55 @@ +package monitor + +import "time" + +type MonitorBase struct { + ID uint `xorm:"id" json:"id"` + CreatedAt time.Time `xorm:"created_at" json:"createdAt"` + UpdatedAt time.Time `xorm:"updated_at" json:"updatedAt"` + + Cpu float64 `xorm:"cpu" json:"cpu"` + + LoadUsage float64 `xorm:"load_usage" json:"loadUsage"` + CpuLoad1 float64 `xorm:"cpu_load1" json:"cpuLoad1"` + CpuLoad5 float64 `xorm:"cpu_load5" json:"cpuLoad5"` + CpuLoad15 float64 `xorm:"cpu_load15" json:"cpuLoad15"` + + Memory float64 `xorm:"memory" json:"memory"` + + DbSize uint `xorm:"db_size" json:"dbSize"` +} + +type MonitorIO struct { + ID uint `xorm:"id" json:"id"` + CreatedAt time.Time `xorm:"created_at" json:"createdAt"` + UpdatedAt time.Time `xorm:"updated_at" json:"updatedAt"` + + Name string `xorm:"name" json:"name"` + Read uint64 `xorm:"read" json:"read"` + Write uint64 `xorm:"write" json:"write"` + Count uint64 `xorm:"count" json:"count"` + Time uint64 `xorm:"time" json:"time"` +} + +type MonitorNetwork struct { + ID uint `xorm:"id" json:"id"` + CreatedAt time.Time `xorm:"created_at" json:"createdAt"` + UpdatedAt time.Time `xorm:"updated_at" json:"updatedAt"` + + Name string `xorm:"name" json:"name"` + Up float64 `xorm:"up" json:"up"` + Down float64 `xorm:"down" json:"down"` +} + +type MonitorSearch struct { + Param string `json:"param" validate:"required,oneof=all cpu memory load io network"` + Info string `json:"info"` + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` +} + +type MonitorData struct { + Param string `json:"param" validate:"required,oneof=cpu memory load io network"` + Date []time.Time `json:"date"` + Value []interface{} `json:"value"` +} diff --git a/features/monitor/monitor/monitor.go b/features/monitor/monitor/monitor.go new file mode 100644 index 0000000..7ce950d --- /dev/null +++ b/features/monitor/monitor/monitor.go @@ -0,0 +1,162 @@ +package monitor + +import ( + "net/http" + "sort" + "strings" + "time" + + "ems.agt/lib/core/utils/ctx" + "ems.agt/lib/dborm" + "ems.agt/lib/services" + "ems.agt/restagent/config" + "github.com/shirou/gopsutil/disk" + "github.com/shirou/gopsutil/net" +) + +// 点击【主机 - 监控】菜单,进入监控报表,直观的了解服务器的运行状态,包含【平均负载】、【CPU性能监控】、【内存使用监控】、【磁盘IO监控】、【网络IO监控】 + +// 可以查看昨天,今天,最近7天,最近30天,自定义时间的监控指标情况。 +// 默认监控是开启的,可以在【面板设置 - 监控】页面中根据需求对监控进行开启和关闭。 +// 监控数据默认保存30天,可以自行修改,也可手动清理该日志。 + +var ( + // IP地址 + UriIPAddr = config.DefaultUriPrefix + "/monitor/{apiVersion}/monitor/ipaddr" + // 可选网络 + UriNetOpt = config.DefaultUriPrefix + "/monitor/{apiVersion}/monitor/netoptions" + // 可选磁盘 + UriIoOpt = config.DefaultUriPrefix + "/monitor/{apiVersion}/monitor/iooptions" + // 加载 + UriLoad = config.DefaultUriPrefix + "/monitor/{apiVersion}/monitor/load" + + // IP地址 + UriIPAddrOAM = config.UriPrefix + "/monitor/{apiVersion}/monitor/ipaddr" + // 可选网络 + UriNetOptOAM = config.UriPrefix + "/monitor/{apiVersion}/monitor/netoptions" + // 可选磁盘 + UriIoOptOAM = config.UriPrefix + "/monitor/{apiVersion}/monitor/iooptions" + // 加载 + UriLoadOAM = config.UriPrefix + "/monitor/{apiVersion}/monitor/load" +) + +// IPAddr IP地址 +func IPAddr(w http.ResponseWriter, r *http.Request) { + ipAddrs := []map[string]string{} + interfaces, err := net.Interfaces() + if err == nil { + for _, iface := range interfaces { + addrs := map[string]string{} + for _, v := range iface.Addrs { + prefix := strings.Split(v.Addr, "/")[0] + if strings.Contains(prefix, "::") { + addrs["IPv6"] = prefix + } + if strings.Contains(prefix, ".") { + addrs["IPv4"] = prefix + } + } + ipAddrs = append(ipAddrs, addrs) + } + } + services.ResponseWithJson(w, 200, ipAddrs) +} + +// Netoptions 可选网络 +func Netoptions(w http.ResponseWriter, r *http.Request) { + netStat, _ := net.IOCounters(true) + var options []string + options = append(options, "all") + for _, net := range netStat { + options = append(options, net.Name) + } + sort.Strings(options) + services.ResponseWithJson(w, 200, options) +} + +// Iooptions 可选磁盘 +func Iooptions(w http.ResponseWriter, r *http.Request) { + diskStat, _ := disk.IOCounters() + var options []string + options = append(options, "all") + for _, net := range diskStat { + options = append(options, net.Name) + } + sort.Strings(options) + services.ResponseWithJson(w, 200, options) +} + +// LoadMonitor 载入监控 +func LoadMonitor(w http.ResponseWriter, r *http.Request) { + // json 請求參數獲取 + var bodyArgs MonitorSearch + err := ctx.ShouldBindJSON(r, &bodyArgs) + if err != nil || dborm.DbClient.XEngine == nil { + services.ResponseErrorWithJson(w, 400, err.Error()) + return + } + + loc := time.Now().Location() + bodyArgs.StartTime = bodyArgs.StartTime.In(loc) + bodyArgs.EndTime = bodyArgs.EndTime.In(loc) + + var backdatas []MonitorData + if bodyArgs.Param == "all" || bodyArgs.Param == "cpu" || bodyArgs.Param == "memory" || bodyArgs.Param == "load" { + var bases []MonitorBase + err := dborm.DbClient.XEngine.Table("monitor_base"). + Where("created_at > ? AND created_at < ?", bodyArgs.StartTime, bodyArgs.EndTime). + Desc("created_at"). + Find(&bases) + if err != nil { + services.ResponseErrorWithJson(w, 400, err.Error()) + return + } + + var itemData MonitorData + itemData.Param = "base" + for _, base := range bases { + itemData.Date = append(itemData.Date, base.CreatedAt) + itemData.Value = append(itemData.Value, base) + } + backdatas = append(backdatas, itemData) + } + if bodyArgs.Param == "all" || bodyArgs.Param == "io" { + var bases []MonitorIO + err := dborm.DbClient.XEngine.Table("monitor_io"). + Where("created_at > ? AND created_at < ?", bodyArgs.StartTime, bodyArgs.EndTime). + Desc("created_at"). + Find(&bases) + if err != nil { + services.ResponseErrorWithJson(w, 400, err.Error()) + return + } + + var itemData MonitorData + itemData.Param = "io" + for _, base := range bases { + itemData.Date = append(itemData.Date, base.CreatedAt) + itemData.Value = append(itemData.Value, base) + } + backdatas = append(backdatas, itemData) + } + if bodyArgs.Param == "all" || bodyArgs.Param == "network" { + var bases []MonitorNetwork + err := dborm.DbClient.XEngine.Table("monitor_network"). + Where("name = ? AND created_at > ? AND created_at < ?", bodyArgs.Info, bodyArgs.StartTime, bodyArgs.EndTime). + Desc("created_at"). + Find(&bases) + if err != nil { + services.ResponseErrorWithJson(w, 400, err.Error()) + return + } + + var itemData MonitorData + itemData.Param = "network" + for _, base := range bases { + itemData.Date = append(itemData.Date, base.CreatedAt) + itemData.Value = append(itemData.Value, base) + } + backdatas = append(backdatas, itemData) + } + services.ResponseWithJson(w, 200, backdatas) +} diff --git a/features/monitor/monitor/task.go b/features/monitor/monitor/task.go new file mode 100644 index 0000000..a8185c3 --- /dev/null +++ b/features/monitor/monitor/task.go @@ -0,0 +1,233 @@ +package monitor + +import ( + "encoding/json" + "fmt" + "strconv" + "time" + + "ems.agt/lib/dborm" + "ems.agt/lib/log" + "ems.agt/restagent/config" + "github.com/robfig/cron/v3" + "github.com/shirou/gopsutil/v3/cpu" + "github.com/shirou/gopsutil/v3/disk" + "github.com/shirou/gopsutil/v3/load" + "github.com/shirou/gopsutil/v3/mem" + "github.com/shirou/gopsutil/v3/net" +) + +type MonitorService struct{} + +type IMonitorService interface { + Run() +} + +func NewIMonitorService() IMonitorService { + return &MonitorService{} +} + +func (m *MonitorService) Run() { + // monitorStatus, _ := dborm.XormGetConfig("SystemMonitor", "MonitorStatus") + // if monitorStatus["value"] == "disable" { + // return + // } + var itemModel MonitorBase + itemModel.CreatedAt = time.Now() + itemModel.UpdatedAt = time.Now() + + totalPercent, _ := cpu.Percent(3*time.Second, false) + if len(totalPercent) == 1 { + itemModel.Cpu = totalPercent[0] + } + cpuCount, _ := cpu.Counts(false) + + loadInfo, _ := load.Avg() + itemModel.CpuLoad1 = loadInfo.Load1 + itemModel.CpuLoad5 = loadInfo.Load5 + itemModel.CpuLoad15 = loadInfo.Load15 + itemModel.LoadUsage = loadInfo.Load1 / (float64(cpuCount*2) * 0.75) * 100 + + memoryInfo, _ := mem.VirtualMemory() + itemModel.Memory = memoryInfo.UsedPercent + + var dataSize int + conf := config.GetYamlConfig() + result, err := dborm.DbClient.XEngine.QueryString("SELECT SUM(data_length) AS data_size FROM information_schema.tables WHERE TABLE_SCHEMA = ?;", conf.Database.Name) + if err != nil { + dataSize = 0 + } else { + v, _ := strconv.Atoi(result[0]["data_size"]) + dataSize = v + } + itemModel.DbSize = uint(dataSize) + + _, errx := dborm.DbClient.XEngine.Table("monitor_base").Insert(itemModel) + if errx != nil { + log.Errorf("Insert basic monitoring data failed, err: %v", err) + } + + go loadDiskIO() + go loadNetIO() + + // 删除保留的记录 + // monitorStoreDays, _ := dborm.XormGetConfig("SystemMonitor", "MonitorStoreDays") + // if monitorStoreDays["value"] != "" { + // return + // } + // storeDays, err := strconv.Atoi(MonitorStoreDays.Value) + // if err != nil { + // timeForDelete := time.Now().AddDate(0, 0, -storeDays) + // DelMonitorBase(timeForDelete) + // DelMonitorIO(timeForDelete) + // DelMonitorNet(timeForDelete) + // } + +} + +func loadDiskIO() { + ioStat, _ := disk.IOCounters() + + time.Sleep(60 * time.Second) + + ioStat2, _ := disk.IOCounters() + var ioList []MonitorIO + for _, io2 := range ioStat2 { + for _, io1 := range ioStat { + if io2.Name == io1.Name { + var itemIO MonitorIO + itemIO.CreatedAt = time.Now() + itemIO.UpdatedAt = time.Now() + + itemIO.Name = io1.Name + if io2.ReadBytes != 0 && io1.ReadBytes != 0 && io2.ReadBytes > io1.ReadBytes { + itemIO.Read = uint64(float64(io2.ReadBytes-io1.ReadBytes) / 60) + } + if io2.WriteBytes != 0 && io1.WriteBytes != 0 && io2.WriteBytes > io1.WriteBytes { + itemIO.Write = uint64(float64(io2.WriteBytes-io1.WriteBytes) / 60) + } + + if io2.ReadCount != 0 && io1.ReadCount != 0 && io2.ReadCount > io1.ReadCount { + itemIO.Count = uint64(float64(io2.ReadCount-io1.ReadCount) / 60) + } + writeCount := uint64(0) + if io2.WriteCount != 0 && io1.WriteCount != 0 && io2.WriteCount > io1.WriteCount { + writeCount = uint64(float64(io2.WriteCount-io1.WriteCount) / 60) + } + if writeCount > itemIO.Count { + itemIO.Count = writeCount + } + + if io2.ReadTime != 0 && io1.ReadTime != 0 && io2.ReadTime > io1.ReadTime { + itemIO.Time = uint64(float64(io2.ReadTime-io1.ReadTime) / 60) + } + writeTime := uint64(0) + if io2.WriteTime != 0 && io1.WriteTime != 0 && io2.WriteTime > io1.WriteTime { + writeTime = uint64(float64(io2.WriteTime-io1.WriteTime) / 60) + } + if writeTime > itemIO.Time { + itemIO.Time = writeTime + } + ioList = append(ioList, itemIO) + break + } + } + } + _, err := dborm.DbClient.XEngine.Table("monitor_io").Insert(ioList) + if err != nil { + log.Errorf("Insert io monitoring data failed, err: %v", err) + } +} + +func loadNetIO() { + netStat, _ := net.IOCounters(true) + netStatAll, _ := net.IOCounters(false) + + time.Sleep(60 * time.Second) + + netStat2, _ := net.IOCounters(true) + var netList []MonitorNetwork + for _, net2 := range netStat2 { + for _, net1 := range netStat { + if net2.Name == net1.Name { + var itemNet MonitorNetwork + itemNet.CreatedAt = time.Now() + itemNet.UpdatedAt = time.Now() + + itemNet.Name = net1.Name + + if net2.BytesSent != 0 && net1.BytesSent != 0 && net2.BytesSent > net1.BytesSent { + itemNet.Up = float64(net2.BytesSent-net1.BytesSent) / 1024 / 60 + } + if net2.BytesRecv != 0 && net1.BytesRecv != 0 && net2.BytesRecv > net1.BytesRecv { + itemNet.Down = float64(net2.BytesRecv-net1.BytesRecv) / 1024 / 60 + } + netList = append(netList, itemNet) + break + } + } + } + netStatAll2, _ := net.IOCounters(false) + for _, net2 := range netStatAll2 { + for _, net1 := range netStatAll { + if net2.Name == net1.Name { + var itemNet MonitorNetwork + itemNet.Name = net1.Name + if net2.BytesSent != 0 && net1.BytesSent != 0 && net2.BytesSent > net1.BytesSent { + itemNet.Up = float64(net2.BytesSent-net1.BytesSent) / 1024 / 60 + } + + if net2.BytesRecv != 0 && net1.BytesRecv != 0 && net2.BytesRecv > net1.BytesRecv { + itemNet.Down = float64(net2.BytesRecv-net1.BytesRecv) / 1024 / 60 + } + netList = append(netList, itemNet) + break + } + } + } + + _, err := dborm.DbClient.XEngine.Table("monitor_network").Insert(netList) + if err != nil { + log.Errorf("Insert network monitoring data failed, err: %v", err) + } +} + +var c *cron.Cron +var monitorCronID int + +func init() { + c = cron.New() + c.Start() + monitorCronID = 0 +} + +// StartMonitor 开始监控任务 removeBefore删除上次任务,间隔interval分钟 +func StartMonitor(removeBefore bool, interval string) error { + if removeBefore { + c.Remove(cron.EntryID(monitorCronID)) + } + + // 读取配置 + if interval == "" { + v, err := dborm.XormGetConfig("SystemMonitor", "sampleTime") + if err != nil { + return err + } + data := make(map[string]any) + err = json.Unmarshal([]byte(v["value_json"].(string)), &data) + if err != nil { + log.Error("json StartMonitor:%s", err.Error()) + return err + } + interval = data["sampleTime"].(string) + } + + imservice := NewIMonitorService() + monitorID, err := c.AddJob(fmt.Sprintf("@every %sm", interval), imservice) + if err != nil { + return err + } + imservice.Run() + monitorCronID = int(monitorID) + return nil +} diff --git a/features/monitor/psnet/psnet.go b/features/monitor/psnet/psnet.go new file mode 100644 index 0000000..78877bc --- /dev/null +++ b/features/monitor/psnet/psnet.go @@ -0,0 +1,93 @@ +package psnet + +import ( + "fmt" + "net" + "net/http" + "time" + + "ems.agt/lib/core/utils/ctx" + "ems.agt/lib/log" + "ems.agt/lib/services" + "ems.agt/lib/wsinfo" + "ems.agt/restagent/config" + "github.com/gorilla/websocket" + "github.com/shirou/gopsutil/process" +) + +var ( + // websockte通信 + UriWs = config.DefaultUriPrefix + "/monitor/{apiVersion}/psnet/ws" + // 停止进程 + UriStop = config.DefaultUriPrefix + "/monitor/{apiVersion}/psnet/stop" + + // 检查ip端口请求 + UriPing = config.DefaultUriPrefix + "/monitor/{apiVersion}/psnet/ping" +) + +// 进程管理 +var wsUpgrade = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +// ProcessWs +func ProcessWs(w http.ResponseWriter, r *http.Request) { + ws, err := wsUpgrade.Upgrade(w, r, nil) + if err != nil { + return + } + wsClient := wsinfo.NewWsClient("processClient", ws) + go wsClient.Read() + go wsClient.Write() +} + +// 停止进程 {"PID":30040} +func StopProcess(w http.ResponseWriter, r *http.Request) { + // json 請求參數獲取 + var bodyArgs struct { + PID int32 `json:"PID" validate:"required"` + } + err := ctx.ShouldBindJSON(r, &bodyArgs) + if err != nil { + log.Error("io.ReadAll is failed:", err) + services.ResponseErrorWithJson(w, 400, err.Error()) + return + } + + proc, err := process.NewProcess(bodyArgs.PID) + if err != nil { + services.ResponseErrorWithJson(w, 400, err.Error()) + return + } + if err := proc.Kill(); err != nil { + services.ResponseErrorWithJson(w, 400, err.Error()) + return + } + services.ResponseStatusOK200Null(w) +} + +// 检查ip端口请求 +func Ping(w http.ResponseWriter, r *http.Request) { + // json 請求參數獲取 + var bodyArgs struct { + Host string `json:"host" validate:"required"` + Port string `json:"port" validate:"required"` + } + err := ctx.ShouldBindJSON(r, &bodyArgs) + if err != nil { + log.Error("io.ReadAll is failed:", err) + services.ResponseErrorWithJson(w, 400, err.Error()) + return + } + + conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%s", bodyArgs.Host, bodyArgs.Port), 3*time.Second) + if err != nil { + services.ResponseErrorWithJson(w, 400, err.Error()) + return + } + defer conn.Close() + services.ResponseStatusOK200Null(w) + +} diff --git a/features/nbi/nbi.go b/features/nbi/nbi.go new file mode 100644 index 0000000..a6a8aab --- /dev/null +++ b/features/nbi/nbi.go @@ -0,0 +1,259 @@ +package nbi + +import ( + "bytes" + "fmt" + "net/http" + "strings" + + "ems.agt/lib/dborm" + "github.com/go-resty/resty/v2" + + "github.com/gorilla/mux" + + "ems.agt/lib/global" + "ems.agt/lib/log" + "ems.agt/lib/oauth" + "ems.agt/lib/services" + "ems.agt/lib/session" + "ems.agt/restagent/config" +) + +type ErrorOAuthResponse struct { + Error map[string]interface{} +} + +type FailOAuthResponse struct { + Error struct { + ErrorCode string + ErrorInfo string + } +} + +type ApiResponse struct { + ResultCode string + ResultMessage interface{} +} + +var globalSession = session.NewSessManager("restagent") + +var ( + MAX_RMUID_NUM int + MAX_ALARMID_NUM int + MAX_PMUID_NUM int + MAX_SUBID_NUM int + MAX_URI_LEN int + RMUID_REGEXP string +) + +var ( + // Northbound interface + GetNRMUri = config.DefaultUriPrefix + "/resourceManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/{objectTypeValue}" + NorthboundGetAlarmUri = config.DefaultUriPrefix + "/faultManagement/{apiVersion}/alarms" // ?alarmIds={alarmIdValues} + + CustomGetNRMUri = config.UriPrefix + "/resourceManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/{objectTypeValue}" +) + +func CheckParameterName(r *http.Request) []string { + var errorParams []string + vars := r.URL.Query() + for k, v := range vars { + log.Debug("vars:", k, v) + if k != "rmUIDs" && k != "fields" { + errorParams = append(errorParams, k) + } + } + + return errorParams +} + +func GetRmUIDArr(r *http.Request) []string { + vars := r.URL.Query() + rmUIDs, ok := vars["rmUIDs"] + if !ok { + log.Debug("rmUIDs is not exist") + return nil + } + + var rmUIDValues []string + for _, r := range rmUIDs { + if r != "" { + rmUIDValues = global.MergeStringArr(rmUIDValues, strings.Split(r, `,`)) + } + } + + return rmUIDValues +} + +func GetAttrNameArr(r *http.Request) []string { + vars := r.URL.Query() + fields, ok := vars["fields"] + if !ok { + log.Debug("attributeNames does not exist") + return nil + } + var attrNames []string + for _, a := range fields { + if a != "" { + attrNames = global.MergeStringArr(attrNames, strings.Split(a, `,`)) + } + } + + return attrNames +} + +func CheckValidRmUID(rmUIDs []string) []string { + log.Debug("CheckValidRmUID processing... ") + + var invalidRmUIDs []string + for _, r := range rmUIDs { + if !global.MatchRmUID(RMUID_REGEXP, r) { + invalidRmUIDs = append(invalidRmUIDs, r) + } + } + + return invalidRmUIDs +} + +func CheckLocalRmUID(rmUIDs []string) string { + log.Debug("GetLocalRmUID processing... ") + + rmUID := config.GetRmUIDFromConfig() + for _, r := range rmUIDs { + if r == rmUID { + return rmUID + } + } + + return "" +} + +func NBIGetNRMFromNF(w http.ResponseWriter, r *http.Request) { + log.Debug("NBIGetNRMFromNF processing... ") + + // response 414-4 uri too long ? (optional) + // todo ... ? + if bytes.Count([]byte(r.RequestURI), nil) > config.GetUriMaxLenFromConfig() { + log.Error("Request Uri too long:", bytes.Count([]byte(r.RequestURI), nil), config.GetUriMaxLenFromConfig()) + services.ResponseRequestURITooLong414UriTooLong(w) + return + } + + // check media type(content type) only support "application/json" + // response 415-1 + if !services.IsVallidContentType(r, config.GetYamlConfig().OMC.CheckContentType) { + log.Debug("Invalid Content-Type") + services.ResponseUnsupportedMediaType415(w) + return + } + + // error processing ... + // 401-1 response + token, ret := oauth.IsCarriedToken(r) + if ret == false { + log.Error("AccessToken is not carried") + services.ResponseUnauthorized401AccessTokenNotCarried(w) + return + } + + // 401-2 response + if dborm.XormExistValidToken(token, config.GetExpiresFromConfig()) == false { + log.Error("AccessToken fails or does not exist") + services.ResponseUnauthorized401AccessTokenNotExist(w) + return + } + + _, err := dborm.XormUpdateSessionShakeTime(token) + if err != nil { + log.Error("Failed to update session table:", err) + services.ResponseUnauthorized401AccessTokenNotExist(w) + return + } + + /* + // response 403 Forbidden, permissions deny + // todo... + plist := globalSession.GetPermissionFromSession(token) + log.Debug("permission list:", plist) + if len(plist) == 0 || plist[0] == false { + log.Debug("User permission deny") + services.ResponseForbidden403NotPermission(w) + return + } + */ + vars := mux.Vars(r) + neType := vars["elementTypeValue"] + + apiVer := vars["apiVersion"] + if apiVer != "v1" { + log.Error("Uri is invalid") + services.ResponseNotFound404UriNotExist(w, r) + return + } + + // response 406-1 + rmUIDValues := GetRmUIDArr(r) + if rmUIDValues == nil { + log.Error("missing parameter: rmUIDs") + services.ResponseNotAcceptable406MissingParam(w) + return + } + + // response 406-2 + errorParams := CheckParameterName(r) + if errorParams != nil { + log.Error("parameter name error: ", errorParams) + services.ResponseNotAcceptable406ParamError(w, errorParams) + return + } + + // response 400-5 + if len(rmUIDValues) == 0 { + log.Error("rmUIDs is wrong or NULL") + services.ResponseBadRequest400WrongParamValue(w) + return + } + + // response 414-1 + if len(rmUIDValues) > config.GetYamlConfig().Params.RmUIDMaxNum { + log.Error("rmUID greater than", config.GetYamlConfig().Params.RmUIDMaxNum) + services.ResponseRequestURITooLong414NRMNumExceed(w, config.GetYamlConfig().Params.RmUIDMaxNum) + return + } + + var response *resty.Response + respMsg := make(map[string]interface{}) + for _, rmUID := range rmUIDValues { + neInfo, err := dborm.XormGetNeInfoByRmUID(neType, rmUID) + if err != nil { + log.Error("dborm.XormGetNeInfo is failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + + requestURI2NF := fmt.Sprintf("http://%s:%v%s", neInfo.Ip, neInfo.Port, r.RequestURI) + log.Debug("requestURI2NF: GET ", requestURI2NF) + + client := resty.New() + response, err = client.R(). + EnableTrace(). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + Get(requestURI2NF) + if err != nil { + log.Error("Failed to Get from NF:", err) + services.ResponseInternalServerError500NFConnectRefused(w) + return + } + switch response.StatusCode() { + case http.StatusOK, http.StatusAccepted, http.StatusNoContent, http.StatusCreated: + respMsg["data"] = response + default: + if response != nil { + services.TransportResponse(w, response.StatusCode(), response.Body()) + } + } + } + + services.TransportResponse(w, response.StatusCode(), response.Body()) +} diff --git a/features/nbi/snmp.go b/features/nbi/snmp.go new file mode 100644 index 0000000..38c9c5c --- /dev/null +++ b/features/nbi/snmp.go @@ -0,0 +1,203 @@ +package nbi + +import ( + "bytes" + "fmt" + "net/http" + "strconv" + + "github.com/gorilla/mux" + g "github.com/gosnmp/gosnmp" + + "ems.agt/lib/log" + "ems.agt/lib/services" + "ems.agt/restagent/config" +) + +func init() { + conf := config.GetYamlConfig() + // Default is a pointer to a GoSNMP struct that contains sensible defaults + // eg port 161, community public, etc + g.Default.Target = conf.NE.Addr + g.Default.Port = conf.NE.Port + err := g.Default.Connect() + if err != nil { + fmt.Printf("Connect() err: %v", err) + } + //defer g.Default.Conn.Close() + MAX_RMUID_NUM = config.GetRmUIDMaxNumFromConfig() + MAX_ALARMID_NUM = config.GetAlarmIDMaxNumFromConfig() + MAX_PMUID_NUM = config.GetPmIDMaxNumFromConfig() + MAX_SUBID_NUM = config.GetSubIDMaxNumFromConfig() + MAX_URI_LEN = config.GetUriMaxLenFromConfig() + RMUID_REGEXP = config.GetRmUIDRegexpFromConfig() +} + +func GetNRMByUri(w http.ResponseWriter, r *http.Request) { + log.Debug("GetNRMByUri processing... ") + + // response 414-4 uri too long ? (optional) + // todo ... ? + if bytes.Count([]byte(r.RequestURI), nil) > MAX_URI_LEN { + log.Error("Request Uri too long:", bytes.Count([]byte(r.RequestURI), nil)) + services.ResponseRequestURITooLong414UriTooLong(w) + return + } + + // check media type(content type) only support "application/json" + // response 415-1 + if !services.IsVallidContentType(r, config.GetYamlConfig().OMC.CheckContentType) { + log.Debug("Invalid Content-Type") + services.ResponseUnsupportedMediaType415(w) + return + } + + // error processing ... + // 401-1 response + token, ret := globalSession.IsCarriedToken(r) + if ret == false { + log.Error("AccessToken is not carried") + services.ResponseUnauthorized401AccessTokenNotCarried(w) + return + } + + // 401-2 response + if globalSession.IsValidToken(token) == false { + log.Error("AccessToken fails or does not exist") + services.ResponseUnauthorized401AccessTokenNotExist(w) + return + } + // response 403 Forbidden, permissions deny + // todo... + plist := globalSession.GetPermissionFromSession(token) + log.Debug("permission list:", plist) + if len(plist) == 0 || plist[0] == false { + log.Error("User permission deny") + services.ResponseForbidden403NotPermission(w) + return + } + + vars := mux.Vars(r) + qeuryUri := vars["apiCategory"] + "/" + vars["elementTypeValue"] + "/" + vars["objectTypeValue"] + log.Debug("Get by Uri: ", qeuryUri) + + apiVer := vars["apiVersion"] + if apiVer != "v1" { + log.Error("Uri is invalid") + services.ResponseNotFound404UriNotExist(w, r) + return + } + + // response 406-1 + rmUIDValues := GetRmUIDArr(r) + if rmUIDValues == nil { + log.Error("missing parameter: rmUIDs") + services.ResponseNotAcceptable406MissingParam(w) + return + } + + // response 406-2 + errorParams := CheckParameterName(r) + if errorParams != nil { + log.Error("parameter name error: ", errorParams) + services.ResponseNotAcceptable406ParamError(w, errorParams) + return + } + + // response 400-5 + if len(rmUIDValues) == 0 { + log.Error("rmUIDs is wrong or NULL") + services.ResponseBadRequest400WrongParamValue(w) + return + } + + // response 414-1 + if len(rmUIDValues) > MAX_RMUID_NUM { + log.Error("rmUID greater than", MAX_RMUID_NUM) + services.ResponseRequestURITooLong414NRMNumExceed(w, MAX_RMUID_NUM) + return + } + + // response 400-1 + // check rmUID is valid + // todo ... + invalidRmUIDs := CheckValidRmUID(rmUIDValues) + if len(invalidRmUIDs) != 0 { + log.Error("rmUID is invalid") + services.ResponseBadRequest400RmUIDsIsInvalid(w, invalidRmUIDs) + return + } + + // response 404-2 + rmUID := CheckLocalRmUID(rmUIDValues) + if rmUID == "" { + log.Error("rmUID does not exist") + services.ResponseNotFound404NRMNotExist(w, rmUIDValues) + return + } + + // response 404-1, uri is not exist in map + attrNames := GetAttrNameArr(r) + var Oids []string + Oids = *config.GetOidByFileds(qeuryUri, attrNames, &Oids) + if len(Oids) == 0 { + log.Error("Nothing of config map") + services.ResponseNotFound404UriNotExist(w, r) + return + } + + // response 404-1, uri is not exist in map + var nameOids []config.NameOid + nameOids = *config.GetDataOidByFields(qeuryUri, attrNames, &nameOids) + if len(nameOids) == 0 { + log.Error("Nothing of config map") + services.ResponseNotFound404UriNotExist(w, r) + return + } + + result, err2 := g.Default.Get(Oids) // Get() accepts up to g.MAX_OIDS + if err2 != nil { + log.Fatalf("Get() err: %v", err2) + + } + + // var nameValues []config.NameValue + var nameValue config.NameValue + + nameValues := make(map[string]interface{}) + nameValues["rmUID"] = rmUID + for i, variable := range result.Variables { + nameValue.Name = nameOids[i].Name + log.Debugf("%d: oid: %s name: %s\n", i, variable.Name, nameValue.Name) + // if nameOids[i].Oid == variable.Name && global.IsContain(attributeNames, nameValue.Name) { + if nameOids[i].Oid == variable.Name { + // the Value of each variable returned by Get() implements + // interface{}. You could do a type switch... + switch variable.Type { + case g.OctetString: + bytes := variable.Value.([]byte) + log.Debugf("string: %s\n", string(bytes)) + nameValue.Value = string(bytes) + nameValues[nameValue.Name] = nameValue.Value + case g.Integer: + value := variable.Value.(int) + log.Debugf("integer: %d\n", value) + nameValue.Value = strconv.Itoa(value) + nameValues[nameValue.Name] = nameValue.Value + case g.IPAddress: + value := variable.Value.(string) + log.Debugf("IPAddress: %s\n", variable.Value) + nameValue.Value = value + nameValues[nameValue.Name] = nameValue.Value + default: + // ... or often you're just interested in numeric values. + // ToBigInt() will return the Value as a BigInt, for plugging + // into your calculations. + log.Debugf("number: %d\n", g.ToBigInt(variable.Value)) + } + } + } + + getResponse := services.DataResponse{nameValues} + services.ResponseWithJson(w, http.StatusOK, getResponse) +} diff --git a/features/pm/performance.go b/features/pm/performance.go new file mode 100644 index 0000000..d7fe84a --- /dev/null +++ b/features/pm/performance.go @@ -0,0 +1,977 @@ +package pm + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strconv" + "time" + + "ems.agt/lib/dborm" + "ems.agt/lib/global" + "ems.agt/lib/log" + "ems.agt/lib/services" + "ems.agt/restagent/config" + "xorm.io/xorm" + + "github.com/go-resty/resty/v2" + _ "github.com/go-sql-driver/mysql" + "github.com/gorilla/mux" +) + +type Response struct { + Data interface{} `json:"data"` +} + +type KpiReport struct { + Timestamp string `json:"TimeStamp"` + Task struct { + Period struct { + StartTime string `json:"StartTime"` + EndTime string `json:"EndTime"` + } `json:"Period"` + NE struct { + NEName string `json:"NEName"` + RmUID string `json:"rmUID"` + NeType string `json:"NeType"` + KPIs []struct { + KPIID string `json:"KPIID"` + Value int `json:"Value"` + Err string `json:"Err"` + } `json:"KPIs"` + } `json:"NE"` + } `json:"Task"` +} + +type GoldKpi struct { + // Id int `json:"-" xorm:"pk 'id' autoincr"` + Date string `json:"date" xorm:"date"` + Index int `json:"index"` + StartTime string `json:"startTime"` + EndTime string `json:"endTime"` + NEName string `json:"neName" xorm:"ne_name"` + RmUid string `json:"rmUid" xorm:"rm_uid"` + NEType string `json:"neType" xorm:"ne_type"` + KpiId string `json:"kpiId" xorm:"kpi_id"` + Value int `json:"value"` + Error string `json:"error"` + Timestamp string `json:"timestamp"` +} + +var ( + // performance management + PerformanceUri = config.DefaultUriPrefix + "/performanceManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/kpiReport/{index}" + MeasureTaskUri = config.DefaultUriPrefix + "/performanceManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/measureTask" + MeasureReportUri = config.DefaultUriPrefix + "/performanceManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/measureReport" + MeasureReportFmt = config.DefaultUriPrefix + "/performanceManagement/v1/elementType/%s/objectType/measureReport" + MeasurementUri = config.DefaultUriPrefix + "/performanceManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/measurement/{index}" + UriMeasureTask = config.DefaultUriPrefix + "/performanceManagement/{apiVersion}/measureTask/{netype}" + + // performance management + CustomPerformanceUri = config.UriPrefix + "/performanceManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/kpiReport/{index}" + CustomMeasureTaskUri = config.UriPrefix + "/performanceManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/measureTask" + CustomMeasureReportUri = config.UriPrefix + "/performanceManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/measureReport" + CustomMeasureReportFmt = config.UriPrefix + "/performanceManagement/v1/elementType/%s/objectType/measureReport" + CustomMeasurementUri = config.UriPrefix + "/performanceManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/measurement/{index}" + CustomUriMeasureTask = config.UriPrefix + "/performanceManagement/{apiVersion}/measureTask/{netype}" +) + +var xEngine *xorm.Engine + +type DatabaseClient struct { + dbType string + dbUrl string + dbConnMaxLifetime time.Duration + dbMaxIdleConns int + dbMaxOpenConns int + IsShowSQL bool + + XEngine *xorm.Engine +} + +var DbClient DatabaseClient + +func InitDbClient(dbType, dbUser, dbPassword, dbHost, dbPort, dbName string) error { + DbClient.dbUrl = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=true&loc=Local", + dbUser, dbPassword, dbHost, dbPort, dbName) + DbClient.dbType = dbType + DbClient.dbConnMaxLifetime = 0 + DbClient.dbMaxIdleConns = 0 + DbClient.dbMaxOpenConns = 0 + if log.GetLevel() == log.LOG_TRACE { + DbClient.IsShowSQL = true + } + log.Debugf("dbType:%s dbUrl:%s:******@tcp(%s:%s)/%s??charset=utf8&parseTime=true&loc=Local", + dbType, dbUser, dbHost, dbPort, dbName) + + var err error + DbClient.XEngine, err = xorm.NewEngine(DbClient.dbType, DbClient.dbUrl) + if err != nil { + log.Error("Failed to connet database:", err) + return err + } + DbClient.XEngine.SetConnMaxLifetime(DbClient.dbConnMaxLifetime) + DbClient.XEngine.SetMaxIdleConns(DbClient.dbMaxIdleConns) + DbClient.XEngine.SetMaxOpenConns(DbClient.dbMaxOpenConns) + if DbClient.IsShowSQL { + DbClient.XEngine.ShowSQL(true) + } + xEngine = DbClient.XEngine + + return nil +} + +func XormConnectDatabase(dbType, dbUser, dbPassword, dbHost, dbPort, dbName string) (*xorm.Engine, error) { + sqlStr := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=true&loc=Local", + dbUser, dbPassword, dbHost, dbPort, dbName) + log.Debugf("dbType:%s Connect to:%s:******@tcp(%s:%s)/%s?charset=utf8&parseTime=true&loc=Local", + dbType, dbUser, dbHost, dbPort, dbName) + var err error + xEngine, err = xorm.NewEngine(dbType, sqlStr) //1、Create xorm engine + if err != nil { + log.Error("Failed to connect database:", err) + return nil, err + } + if log.GetLevel() == log.LOG_TRACE { + xEngine.ShowSQL(true) + } + return xEngine, nil +} + +func GetDateFromTimeString(fmtString string, timeString string) string { + t, _ := time.ParseInLocation(fmtString, timeString, time.Local) + return t.Format("2006-01-02") +} + +func GetDateTimeFromTimeString(fmtString string, timeString string) string { + t, _ := time.ParseInLocation(fmtString, timeString, time.Local) + return t.Format(global.DateTime) +} + +// process alarm post message from NFs +func PostKPIReportFromNF(w http.ResponseWriter, r *http.Request) { + log.Debug("PostKPIReportFromNF processing... ") + + vars := mux.Vars(r) + apiVer := vars["apiVersion"] + if apiVer != global.ApiVersionV1 { + log.Error("Uri api version is invalid. apiVersion:", apiVer) + services.ResponseNotFound404UriNotExist(w, r) + return + } + + // body, err := io.ReadAll(io.LimitReader(r.Body, global.RequestBodyMaxLen)) + body, err := io.ReadAll(io.LimitReader(r.Body, global.RequestBodyMaxLen)) + if err != nil { + log.Error("Faile to io.ReadAll: ", err) + services.ResponseNotFound404UriNotExist(w, r) + return + } + log.Debug("Request body:", string(body)) + kpiReport := new(KpiReport) + _ = json.Unmarshal(body, &kpiReport) + log.Debug("kpiReport:", kpiReport) + + session := xEngine.NewSession() + defer session.Close() + goldKpi := new(GoldKpi) + layout := time.RFC3339Nano + goldKpi.Date = GetDateFromTimeString(layout, kpiReport.Task.Period.StartTime) + goldKpi.Index, _ = strconv.Atoi(vars["index"]) + goldKpi.StartTime = global.GetFmtTimeString(layout, kpiReport.Task.Period.StartTime, time.DateTime) + goldKpi.EndTime = global.GetFmtTimeString(layout, kpiReport.Task.Period.EndTime, time.DateTime) + goldKpi.NEName = kpiReport.Task.NE.NEName + goldKpi.RmUid = kpiReport.Task.NE.RmUID + goldKpi.NEType = kpiReport.Task.NE.NeType + goldKpi.Timestamp = global.GetFmtTimeString(layout, kpiReport.Timestamp, time.DateTime) + for _, k := range kpiReport.Task.NE.KPIs { + goldKpi.KpiId = k.KPIID + goldKpi.Value = k.Value + goldKpi.Error = k.Err + log.Trace("goldKpi:", goldKpi) + + // 启动事务 + err := session.Begin() + if err != nil { + log.Error("Failed to Begin gold_kpi:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + gkpi := &GoldKpi{} + _, err = session.Where("id = ?", 1).ForUpdate().Get(gkpi) + if err != nil { + // 回滚事务 + session.Rollback() + log.Error("Failed to ForUpdate gold_kpi:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + affected, err := session.Insert(goldKpi) + if err != nil && affected <= 0 { + session.Rollback() + log.Error("Failed to insert gold_kpi:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + // 提交事务 + err = session.Commit() + if err != nil { + log.Error("Failed to Commit gold_kpi:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + } + + services.ResponseStatusOK200Null(w) +} + +type MeasureTask struct { + Tasks []Task `json:"Tasks"` + NotifyUrl string `json:"NotifyUrl"` /* "http://xEngine.xEngine.xEngine.x:xxxx/api/rest/performanceManagement/v1/elementType/smf/objectType/measureReport */ +} + +type Task struct { + Id int `json:"Id"` + + StartTime string `json:"StartTime"` + EndTime string `json:"EndTime"` + + Schedule struct { + Type string `json:"Type"` // 计划类型:Weekly/Monthly, 如果type为"", 则任务以StartTime和EndTime为条件进行统计, 否则以Shedule方式进行 + Days []int `json:"Days"` // Weekly: [0,1,...,5,6] 星期日为0, Monthly: [1,2,3,...,30,31] + Periods []dborm.Period `json:"Periods"` + /* + Periods []struct { + Start string `json:"Start"` // 零点或者零点加测量粒度的整数倍 + End string `json:"End"` //零点加测量粒度的整数倍 + } `json:"Periods"` + */ + } `json:"Schedule"` + + GranulOption string `json:"GranulOption"` // 测量粒度选项:15M/30M/60M/24H + KPISet []dborm.KpiSetJ `json:"KPISet"` + /* + KPISet []struct { + Code string `json:"Code"` // 统计编码 如:SMFHA01 + KPIs []string `json:"KPIs` // 指标项集合 ["SMF.AttCreatePduSession", "SMF.AttCreatePduSession._Dnn"] + } `json:"KPISet"` + */ +} + +type MeasureReport struct { + Id int `json:"Id"` + TimeStamp string `json:"TimeStamp"` + NeName string `json:"NeName"` + RmUID string `json:"rmUID"` + NeType string `json:"NeType"` + + Report struct { + Period struct { + StartTime string `json:"StartTime"` + EndTime string `json:"EndTime"` + } `json:"Period"` + + Datas []struct { + Code string `json:"Code"` // 统计编码 如:SMFHA01 + KPIs []struct { + KPIID string `json:"KPIID"` // 指标项, 如: SMF.AttCreatePduSession._Dnn + KPIValues []struct { + Name string `json:"Name"` // 单个的写"Total", 或者指标项有多个测量项,如Dnn的名称写对应的Dnn"cmnet"/"ims" + Value int64 `json:"Value"` + } `json:"KPIValues"` + } `json:"KPIs"` + } `json:"Datas"` + } `json:"Report"` +} + +type MeasureData struct { + // Id int `json:"id" xorm:"pk 'id' autoincr"` + Id int `json:"id" xorm:"-"` + Date string `json:"date" xorm:"date"` + TaskId int `json:"taskId"` + NeType string `json:"neType" xorm:"ne_type"` + NeName string `json:"neName" xorm:"ne_name"` + RmUid string `json:"rmUid" xorm:"rm_uid"` + GranulOption string `json:"granulOption" xorm:"granul_option"` + StartTime string `json:"startTime"` + EndTime string `json:"endTime"` + KpiCode string `json:"kpiCode" xorm:"kpi_code"` + KpiId string `json:"kpiId" xorm:"kpi_id"` + KpiExt string `json:"kpiExt" xorm:"kpi_ext"` + Value int64 `json:"value"` + Timestamp string `json:"timestamp"` +} + +// process measure report from NFs +func PostMeasureReportFromNF(w http.ResponseWriter, r *http.Request) { + log.Debug("PostMeasureReportFromNF processing... ") + + // vars := mux.Vars(r) + // neType := vars["elementTypeValue"] + vars := mux.Vars(r) + apiVer := vars["apiVersion"] + if apiVer != global.ApiVersionV1 { + log.Error("Uri api version is invalid. apiVersion:", apiVer) + services.ResponseNotFound404UriNotExist(w, r) + return + } + + // body, err := io.ReadAll(io.LimitReader(r.Body, global.RequestBodyMaxLen)) + body, err := io.ReadAll(io.LimitReader(r.Body, global.RequestBodyMaxLen)) + if err != nil { + log.Error("Faile to io.ReadAll: ", err) + services.ResponseNotFound404UriNotExist(w, r) + return + } + log.Debug("Request body:", string(body)) + measureReport := new(MeasureReport) + _ = json.Unmarshal(body, &measureReport) + log.Debug("measureReport:", measureReport) + + session := xEngine.NewSession() + defer session.Close() + measureData := new(MeasureData) + layout := global.DateTime + measureData.Date = GetDateFromTimeString(layout, measureReport.Report.Period.StartTime) + measureData.TaskId = measureReport.Id + measureData.StartTime = measureReport.Report.Period.StartTime + measureData.EndTime = measureReport.Report.Period.EndTime + measureData.NeType = measureReport.NeType + measureData.NeName = measureReport.NeName + measureData.RmUid = measureReport.RmUID + measureData.GranulOption, _ = dborm.XormGetSingleCol("measure_task", "granul_option", fmt.Sprintf("id=%d", measureReport.Id)) + t, _ := strconv.ParseInt(measureReport.TimeStamp, 10, 64) + timestamp := time.Unix(t, 0) + log.Debug("timestamp:", timestamp.Format(layout)) + measureData.Timestamp = timestamp.Format(layout) + log.Debug("Datas:", measureReport.Report.Datas) + for _, d := range measureReport.Report.Datas { + measureData.KpiCode = d.Code + + log.Debug("KPIs:", d.KPIs) + for _, k := range d.KPIs { + measureData.KpiId = k.KPIID + + log.Debug("KPIValues:", k.KPIValues) + if len(k.KPIValues) != 0 { + for _, v := range k.KPIValues { + measureData.KpiExt = v.Name + measureData.Value = v.Value + log.Debug("measureData:", measureData) + + affected, err := session.Insert(measureData) + if err != nil && affected <= 0 { + log.Error("Failed to insert measure_data:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + } + } else { + measureData.Value = 0 + log.Debug("measureData:", measureData) + + affected, err := session.Insert(measureData) + if err != nil && affected <= 0 { + log.Error("Failed to insert measure_data:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + } + } + } + + services.ResponseStatusOK204NoContent(w) +} + +func PostMeasureTaskToNF(w http.ResponseWriter, r *http.Request) { + log.Debug("PostMeasureTaskToNF processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + vars := mux.Vars(r) + neType := vars["elementTypeValue"] + params := r.URL.Query() + taskIds := params["id"] + log.Debug("taskIds:", taskIds) + + var response *resty.Response + client := resty.New() + measureTask := new(MeasureTask) + measureTask.Tasks = make([]Task, 1) + for _, taskId := range taskIds { + id, _ := strconv.Atoi(taskId) + task, err := dborm.GetMeasureTask(id) + if err != nil { + log.Error("Failed to connect database: ", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + log.Debug("Table Task:", task) + + measureTask.Tasks[0].Id = task.Id + measureTask.Tasks[0].StartTime = task.StartTime + measureTask.Tasks[0].EndTime = task.EndTime + // v := new(dborm.ScheduleJson) + // _ = json.Unmarshal(task.Schedule, v) + // measureTask.Task[0].Schedule.Type = v.Type + // measureTask.Task[0].Schedule.Days = v.Days + if len(task.Schedule) >= 1 { + measureTask.Tasks[0].Schedule.Type = task.Schedule[0].Type + measureTask.Tasks[0].Schedule.Days = task.Schedule[0].Days + } + //v := new(dborm.ScheduleJ) + //_ = json.Unmarshal(task.Schedule, v) + measureTask.Tasks[0].Schedule.Periods = task.Periods + measureTask.Tasks[0].GranulOption = task.GranulOption + + measureTask.Tasks[0].KPISet = task.KpiSet + ips, err := global.GetIps() + if err != nil { + log.Error("Failed to get local IP:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + log.Debug("ips:", ips) + + measureTask.NotifyUrl = global.SetNotifyUrl(ips[0], config.GetYamlConfig().Rest[0].Port, fmt.Sprintf(MeasureReportFmt, neType)) + log.Debug("Measure Task to NF:", measureTask) + + if len(task.NeIds) == 0 { + var neInfos []dborm.NeInfo + err := dborm.XormGetNeInfoByNeType(neType, &neInfos) + if err != nil { + log.Error("Failed to dborm.XormGetNeInfoByNeType:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + for _, neInfo := range neInfos { + task.NeIds = append(task.NeIds, neInfo.NeId) + } + } + + for _, neId := range task.NeIds { + var err error + neInfo, err := dborm.XormGetNeInfo(neType, neId) + if err != nil { + log.Error("Failed to dborm.XormGetNeInfo:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + if neInfo == nil { + err := errors.New(fmt.Sprintf("not found target NE neType=%s, neId=%s", neType, neId)) + log.Error(err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + requestURI2NF := fmt.Sprintf("http://%s:%v%s", neInfo.Ip, neInfo.Port, r.RequestURI) + log.Debug("requestURI2NF: POST ", requestURI2NF) + + switch task.Status { + case dborm.MeasureTaskStatusInactive: + body, _ := json.Marshal(measureTask) + log.Debug("body: ", string(body)) + + log.Debug("User-Agent: ", config.GetDefaultUserAgent()) + response, err = client.R(). + EnableTrace(). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + SetBody(body). + SetContentLength(true). + Post(requestURI2NF) + + if err != nil { + log.Error("Post to NF failed:", err) + services.ResponseInternalServerError500NFConnectRefused(w) + return + } + log.Debug("response info: ") + log.Debug("Status Code:", response.StatusCode()) + log.Debug("Status:", response.Status()) + log.Debug("Proto:", response.Proto()) + log.Debug("Time:", response.Time()) + log.Debug("Received At:", response.ReceivedAt()) + log.Debug("Size:", response.Size()) + + case dborm.MeasureTaskStatusSuspend: + body, _ := json.Marshal(measureTask) + log.Debug("body: ", string(body)) + response, err = client.R(). + EnableTrace(). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + SetBody(body). + SetContentLength(true). + Put(requestURI2NF) + + if err != nil { + log.Error("Put to NF failed:", err) + services.ResponseInternalServerError500NFConnectRefused(w) + return + } + default: + err = errors.New(fmt.Sprintf("measure task status must be inactive id=%d", id)) + log.Error("Unable to active measure task:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + log.Debug("StatusCode: ", response.StatusCode()) + switch response.StatusCode() { + case http.StatusOK, http.StatusCreated, http.StatusNoContent, http.StatusAccepted: + taskInfo := new(dborm.MeasureTask) + taskInfo.Status = dborm.MeasureTaskStatusActive + taskInfo.CreateTime = time.Now().Format(time.DateTime) + affected, err := dborm.XormUpdateTableById(id, dborm.TableNameMeasureTask, taskInfo) + if err != nil { + log.Error("dborm.XormUpdateTableById is failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } else if affected <= 0 { + log.Info("Not record affected in measure_task") + } + default: + log.Error("NF return failure to active measure task") + if response != nil { + log.Info("response body:", string(response.Body())) + services.TransportResponse(w, response.StatusCode(), response.Body()) + return + } else { + err = errors.New(fmt.Sprintf("failed to active measure task, NF return error status=%v", response.Status())) + log.Error("Unable to active measure task:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + } + } + } + + services.ResponseStatusOK204NoContent(w) +} + +func PutMeasureTaskToNF(w http.ResponseWriter, r *http.Request) { + + services.ResponseStatusOK200Null(w) +} + +func DeleteMeasureTaskToNF(w http.ResponseWriter, r *http.Request) { + log.Debug("DeleteMeasureTaskToNF processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + vars := mux.Vars(r) + neType := vars["elementTypeValue"] + params := r.URL.Query() + taskIds := params["id"] + log.Debug("taskIds:", taskIds) + + var response *resty.Response + respMsg := make(map[string]interface{}) + for _, taskId := range taskIds { + id, _ := strconv.Atoi(taskId) + task, err := dborm.GetMeasureTask(id) + if err != nil { + log.Error("Failed to connect database: ", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + log.Debug("Measure Task:", task) + + if len(task.NeIds) == 0 { + var neInfos []dborm.NeInfo + err := dborm.XormGetNeInfoByNeType(neType, &neInfos) + if err != nil { + log.Error("Failed to dborm.XormGetNeInfoByNeType:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + for _, neInfo := range neInfos { + task.NeIds = append(task.NeIds, neInfo.NeId) + } + } + log.Debug("neIds:", task.NeIds) + if len(task.NeIds) == 0 { + log.Warn("Not found target NE in the measure task") + taskInfo := new(dborm.MeasureTask) + taskInfo.Status = dborm.MeasureTaskStatusDeleted + affected, err := dborm.XormUpdateTableById(id, dborm.TableNameMeasureTask, taskInfo) + if err != nil { + log.Error("dborm.XormUpdateTableById is failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } else if affected <= 0 { + log.Info("Not record affected in measure_task") + } + services.ResponseStatusOK204NoContent(w) + return + } + + for _, neId := range task.NeIds { + var err error + neInfo, err := dborm.XormGetNeInfo(neType, neId) + if err != nil { + log.Error("dborm.XormGetNeInfo is failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + if neInfo != nil { + requestURI2NF := fmt.Sprintf("http://%s:%v%s", neInfo.Ip, neInfo.Port, r.RequestURI) + log.Debug("requestURI2NF: DELETE ", requestURI2NF) + client := resty.New() + response, err = client.R(). + EnableTrace(). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + Delete(requestURI2NF) + if err != nil { + // to avoid can't delete the task for abnormal NF + log.Error("Failed to resty delete:", err) + taskInfo := new(dborm.MeasureTask) + taskInfo.Status = dborm.MeasureTaskStatusDeleted + affected, err := dborm.XormUpdateTableById(id, dborm.TableNameMeasureTask, taskInfo) + if err != nil { + log.Error("dborm.XormUpdateTableById is failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } else if affected <= 0 { + log.Info("Not record affected in measure_task") + } + services.ResponseStatusOK204NoContent(w) + return + } + + log.Info("StatusCode: ", response.StatusCode()) + switch response.StatusCode() { + case http.StatusOK, http.StatusCreated, http.StatusNoContent, http.StatusAccepted: + taskInfo := new(dborm.MeasureTask) + taskInfo.Status = dborm.MeasureTaskStatusDeleted + affected, err := dborm.XormUpdateTableById(id, dborm.TableNameMeasureTask, taskInfo) + if err != nil { + log.Error("dborm.XormUpdateTableById is failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } else if affected <= 0 { + log.Infof("Not record affected in measure_task") + } + services.ResponseStatusOK204NoContent(w) + return + default: + log.Info("response body:", string(response.Body())) + body := new(map[string]interface{}) + _ = json.Unmarshal(response.Body(), &body) + respMsg["error"] = body + } + } else { + taskInfo := new(dborm.MeasureTask) + taskInfo.Status = dborm.MeasureTaskStatusDeleted + affected, err := dborm.XormUpdateTableById(id, dborm.TableNameMeasureTask, taskInfo) + if err != nil { + log.Error("dborm.XormUpdateTableById is failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } else if affected <= 0 { + log.Info("Not record affected in measure_task") + } + services.ResponseStatusOK204NoContent(w) + return + } + } + } + + services.ResponseWithJson(w, response.StatusCode(), respMsg) +} + +func PatchMeasureTaskToNF(w http.ResponseWriter, r *http.Request) { + log.Debug("PatchMeasureTaskToNF processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + vars := mux.Vars(r) + neType := vars["elementTypeValue"] + params := r.URL.Query() + taskIds := params["id"] + log.Debug("taskIds:", taskIds) + + var response *resty.Response + respMsg := make(map[string]interface{}) + for _, taskId := range taskIds { + id, _ := strconv.Atoi(taskId) + task, err := dborm.GetMeasureTask(id) + if err != nil { + log.Error("Failed to connect database: ", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + log.Debug("Measure Task:", task) + + // for neType + if len(task.NeIds) == 0 { + var neInfos []dborm.NeInfo + err := dborm.XormGetNeInfoByNeType(neType, &neInfos) + if err != nil { + log.Error("Failed to dborm.XormGetNeInfoByNeType:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + for _, neInfo := range neInfos { + task.NeIds = append(task.NeIds, neInfo.NeId) + } + } + + if len(task.NeIds) == 0 { + taskInfo := new(dborm.MeasureTask) + taskInfo.Status = dborm.MeasureTaskStatusInactive + affected, err := dborm.XormUpdateTableById(id, dborm.TableNameMeasureTask, taskInfo) + if err != nil { + log.Error("dborm.XormUpdateTableById is failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } else if affected <= 0 { + log.Info("Not record affected in measure_task") + } + services.ResponseStatusOK204NoContent(w) + return + } + + for _, neId := range task.NeIds { + var err error + neInfo, err := dborm.XormGetNeInfo(neType, neId) + if err != nil { + log.Error("dborm.XormGetNeInfo is failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + if neInfo == nil { + em := errors.New("Not found NE info in database") + log.Error(em) + taskInfo := new(dborm.MeasureTask) + taskInfo.Status = dborm.MeasureTaskStatusInactive + affected, err := dborm.XormUpdateTableById(id, dborm.TableNameMeasureTask, taskInfo) + if err != nil { + log.Error("dborm.XormUpdateTableById is failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } else if affected <= 0 { + log.Info("Not record affected in measure_task") + } + services.ResponseStatusOK204NoContent(w) + //services.ResponseInternalServerError500ProcessError(w, em) + return + } + + requestURI2NF := fmt.Sprintf("http://%s:%v%s", neInfo.Ip, neInfo.Port, r.RequestURI) + log.Debug("requestURI2NF: PATCH ", requestURI2NF) + client := resty.New() + response, err = client.R(). + EnableTrace(). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + Patch(requestURI2NF) + if err != nil { + log.Error("Patch to NF failed:", err) + services.ResponseInternalServerError500NFConnectRefused(w) + return + } + + log.Debug("StatusCode: ", response.StatusCode()) + switch response.StatusCode() { + case http.StatusOK, http.StatusCreated, http.StatusNoContent, http.StatusAccepted: + taskInfo := new(dborm.MeasureTask) + taskInfo.Status = dborm.MeasureTaskStatusInactive + affected, err := dborm.XormUpdateTableById(id, dborm.TableNameMeasureTask, taskInfo) + if err != nil { + log.Error("dborm.XormUpdateTableById is failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } else if affected <= 0 { + log.Info("Not record affected in measure_task") + } + default: + log.Debug("response body:", string(response.Body())) + body := new(map[string]interface{}) + _ = json.Unmarshal(response.Body(), &body) + respMsg["error"] = body + } + } + } + + services.ResponseWithJson(w, response.StatusCode(), respMsg) + return +} + +type Measurement struct { + Id int `json:"-" xorm:"pk 'id' autoincr"` + Date string `json:"-" xorm:"date"` + Index int `json:"Index"` // 1天中测量时间粒度(如15分钟)的切片索引: 0~95 + Timestamp string `json:"TimeStamp" xorm:"-"` + NeName string `json:"NeName"` // UserLabel + RmUID string `json:"RmUID" xorm:"rm_uid"` + NeType string `json:"NeType"` // 网元类型 + PmVersion string `json:"PmVersion"` // 性能数据版本号 + Dn string `json:"Dn"` // (???)网元标识, 如:RJN-CMZJ-TZ,SubNetwork=5GC88,ManagedElement=SMF53456,SmfFunction=53456 + Period string `json:"Period"` // 测量时间粒度选项:5/15/30/60 + TimeZone string `json:"TimeZone"` + StartTime string `json:"StartTime"` + + Datas []Data `json:"Datas"` +} + +type KPIValue struct { + Name string `json:"Name"` // 单个的写"Total", 或者指标项有多个测量项,如Dnn的名称写对应的Dnn"cmnet"/"ims" + Value int64 `json:"Value"` +} + +type KPI struct { + KPIID string `json:"KPIID"` // 指标项, 如: SMF.AttCreatePduSession._Dnn + KPIValues []KPIValue `json:"KPIValues"` +} + +type Data struct { + ObjectType string `json:"ObjectType"` // 网络资源类别名称, Pm指标项列表中为空间粒度 如:SmfFunction + KPIs []KPI `json:"KPIs"` // 指标项, 如: SMF.AttCreatePduSession._Dnn +} + +// process measurement post message from NFs +func PostMeasurementFromNF(w http.ResponseWriter, r *http.Request) { + log.Debug("PostMeasurementFromNF processing... ") + + vars := mux.Vars(r) + apiVer := vars["apiVersion"] + if apiVer != global.ApiVersionV1 { + log.Error("Uri api version is invalid. apiVersion:", apiVer) + services.ResponseNotFound404UriNotExist(w, r) + return + } + + body, err := io.ReadAll(io.LimitReader(r.Body, global.RequestBodyMaxLen)) + if err != nil { + log.Error("Faile to io.ReadAll: ", err) + services.ResponseNotFound404UriNotExist(w, r) + return + } + log.Debug("Request body:", string(body)) + // measurement := new(dborm.NorthboundPm) + measurement := new(dborm.NorthboundPm) + _ = json.Unmarshal(body, &measurement) + log.Debug("measurement:", measurement) + + session := dborm.DbClient.XEngine.NewSession() + defer session.Close() + + // layout := global.DateTime + layout := time.RFC3339 + measurement.Date = GetDateFromTimeString(layout, measurement.StartTime) + measurement.StartTime = GetDateTimeFromTimeString(layout, measurement.StartTime) + affected, err := session.Table("northbound_pm").Insert(measurement) + if err != nil && affected <= 0 { + log.Error("Failed to insert northbound_pm:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + + services.ResponseStatusOK204NoContent(w) +} + +// get measurement message from NFs +func GetMeasurementFromNF(w http.ResponseWriter, r *http.Request) { + log.Debug("GetMeasurementFromNF processing... ") + + _, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + + vars := mux.Vars(r) + apiVer := vars["apiVersion"] + if apiVer != global.ApiVersionV1 { + log.Error("Uri api version is invalid. apiVersion:", apiVer) + services.ResponseNotFound404UriNotExist(w, r) + return + } + + neType := vars["elementTypeValue"] + if neType == "" { + log.Error("elementTypeValue is null.") + services.ResponseNotFound404UriNotExist(w, r) + return + } + params := r.URL.Query() + neIds := params["ne_id"] + if len(neIds) == 0 { + log.Error("ne_id NOT FOUND") + services.ResponseBadRequest400WrongParamValue(w) + return + } + log.Debugf("neType: %s neId:%s", neType, neIds) + + //var neInfo *dborm.NeInfo + neInfo := new(dborm.NeInfo) + + neInfo, err = dborm.XormGetNeInfo(neType, neIds[0]) + if err != nil { + log.Error("dborm.XormGetNeInfo is failed:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + + requestURI2NF := fmt.Sprintf("http://%s:%v%s", neInfo.Ip, neInfo.Port, r.RequestURI) + log.Debug("requestURI2NF: GET ", requestURI2NF) + + client := resty.New() + response, err := client.R(). + EnableTrace(). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + Get(requestURI2NF) + if err != nil { + log.Error("Failed to Get from NF:", err) + services.ResponseInternalServerError500NFConnectRefused(w) + return + } + + respMsg := make(map[string]interface{}) + switch response.StatusCode() { + case http.StatusOK, http.StatusCreated, http.StatusNoContent, http.StatusAccepted: + log.Debug("response:", response) + // measurement := new(dborm.NorthboundPm) + measurement := new(dborm.NorthboundPm) + _ = json.Unmarshal(response.Body(), &measurement) + log.Debug("measurement:", measurement) + + session := dborm.DbClient.XEngine.NewSession() + defer session.Close() + + layout := time.RFC3339 + measurement.Date = GetDateFromTimeString(layout, measurement.StartTime) + measurement.StartTime = GetDateTimeFromTimeString(layout, measurement.StartTime) + affected, err := session.Table("northbound_pm").Insert(measurement) + if err != nil && affected <= 0 { + log.Error("Failed to insert northbound_pm:", err) + services.ResponseInternalServerError500DatabaseOperationFailed(w) + return + } + default: + log.Debug("response body:", string(response.Body())) + body := new(map[string]interface{}) + _ = json.Unmarshal(response.Body(), &body) + respMsg["error"] = body + } + + services.ResponseWithJson(w, response.StatusCode(), respMsg) +} diff --git a/features/security/account.go b/features/security/account.go new file mode 100644 index 0000000..36a36ea --- /dev/null +++ b/features/security/account.go @@ -0,0 +1,409 @@ +package security + +import ( + "encoding/json" + "fmt" + "image/color" + "io" + "net/http" + "strconv" + "strings" + "time" + + "ems.agt/features/security/service" + sysConfigService "ems.agt/features/sys_config/service" + "ems.agt/lib/core/account" + "ems.agt/lib/core/cache" + "ems.agt/lib/core/conf" + "ems.agt/lib/core/constants/cachekey" + "ems.agt/lib/core/utils/ctx" + "ems.agt/lib/core/vo/result" + "ems.agt/lib/dborm" + "ems.agt/lib/global" + "ems.agt/lib/log" + "ems.agt/lib/oauth" + "ems.agt/lib/services" + "ems.agt/restagent/config" + "github.com/go-admin-team/go-admin-core/logger" + "github.com/mojocn/base64Captcha" +) + +var ( + UriOauthToken = config.DefaultUriPrefix + "/securityManagement/{apiVersion}/{elementTypeValue}/token" + UriOauthHandshake = config.DefaultUriPrefix + "/securityManagement/{apiVersion}/{elementTypeValue}/handshake" + + CustomUriOauthToken = config.UriPrefix + "/securityManagement/{apiVersion}/{elementTypeValue}/token" + CustomUriOauthHandshake = config.UriPrefix + "/securityManagement/{apiVersion}/{elementTypeValue}/handshake" + + // 系统登录 + UriLogin = config.DefaultUriPrefix + "/securityManagement/{apiVersion}/login" + CustomUriLogin = config.UriPrefix + "/securityManagement/{apiVersion}/login" + + // 获取验证码 + UriCaptchaImage = config.DefaultUriPrefix + "/securityManagement/{apiVersion}/captchaImage" + CustomUriCaptchaImage = config.UriPrefix + "/securityManagement/{apiVersion}/captchaImage" + + // 登录用户信息 + UriUserInfo = config.DefaultUriPrefix + "/securityManagement/{apiVersion}/getUserInfo" + CustomUriUserInfo = config.UriPrefix + "/securityManagement/{apiVersion}/getUserInfo" + + // 登录用户路由信息 + UriRouters = config.DefaultUriPrefix + "/securityManagement/{apiVersion}/getRouters" + CustomUriRouters = config.UriPrefix + "/securityManagement/{apiVersion}/getRouters" +) + +func LoginFromOMC(w http.ResponseWriter, r *http.Request) { + log.Info("LoginFromOMC processing... ") + + body, err := io.ReadAll(io.LimitReader(r.Body, global.RequestBodyMaxLen)) //io.LimitReader限制大小 + if err != nil { + log.Error("Failed to ReadAll:", err) + services.ResponseNotFound404UriNotExist(w, r) + return + } + + // check media type(content type) only support "application/json" + if !services.IsVallidContentType(r, config.GetYamlConfig().OMC.CheckContentType) { + log.Debug("Invalid Content-Type") + services.ResponseUnsupportedMediaType415(w) + return + } + + // // check extend uri, response 404 + // if !IsValidOAuthUri(r) { + // log.Debug("Uri is invalid") + // services.ResponseNotFound404UriNotExist(w, r) + // return + // } + + // Error process .... + // response 400-7 + if !json.Valid([]byte(body)) { + log.Error("Invalid Json Format") + services.ResponseBadRequest400InvalidJson(w) + return + } + + var oAuthBody oauth.OAuthBody + _ = json.Unmarshal(body, &oAuthBody) //转为json + //log.Debug("body:", string(body), "oAuthBody:", oAuthBody) + + defer r.Body.Close() + // response 400-5 + if oauth.IsWrongOAuthInfo(oAuthBody) { + log.Error("Wrong parameter value") + services.ResponseBadRequest400WrongParamValue(w) + return + } + /* + if oauth.IsValidOAuthInfo(oAuthBody) { + plist := config.GetPermissionFromConfig(oAuthBody.UserName, oAuthBody.GrantType) + log.Debug("Permission list:", plist) + + token := globalSession.NewSession(w, r, plist) + services.ResponseStatusOK200Login(w, token) + } else { + // response 400-4 + log.Debug("Authentication failed, mismatch user or password") + + services.ResponseBadRequest400IncorrectLogin(w) + } + */ + validUser, user, err := dborm.XormCheckLoginUser(oAuthBody.UserName, + oAuthBody.Value, config.GetYamlConfig().Auth.Crypt) + if !validUser || err != nil { + // response 400-4 + log.Error("Authentication failed, mismatch user or password") + services.ResponseErrorWithJson(w, 400, err.Error()) + return + } + + token := oauth.GenRandToken("omc") // Generate new token to session ID + sourceAddr := r.RemoteAddr[:strings.Index(r.RemoteAddr, ":")] + affected, err := dborm.XormInsertSession(oAuthBody.UserName, sourceAddr, token, + config.GetExpiresFromConfig(), config.GetYamlConfig().Auth.Session) + if err != nil { + log.Error("Failed to XormInsertSession:", err) + if affected == -1 { + services.ResponseForbidden403MultiLoginNotAllowed(w) + } else { + services.ResponseBadRequest400IncorrectLogin(w) + } + + return + } + + if user != nil { + // 缓存用户信息 + account.CacheLoginUser(user) + // 角色权限集合,管理员拥有所有权限 + userId := fmt.Sprint(user.Id) + isAdmin := conf.IsAdmin(userId) + roles, perms := service.NewServiceAccount.RoleAndMenuPerms(userId, isAdmin) + services.ResponseStatusOK200LoginWhitRP(w, token, user, roles, perms) + return + } + services.ResponseBadRequest400IncorrectLogin(w) +} + +func LogoutFromOMC(w http.ResponseWriter, r *http.Request) { + log.Info("LogoutFromOMC processing... ") + + token, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + // // check media type(content type) only support "application/json" + // if services.IsVallidContentType(r, config.GetYamlConfig().OMC.CheckContentType) == false { + // log.Error("Invalid Content-Type") + // services.ResponseUnsupportedMediaType415(w) + // return + // } + + // // check extend uri, response 404 + // if !services.IsValidOAuthUri(r) { + // log.Error("Uri is invalid") + // services.ResponseNotFound404UriNotExist(w, r) + // return + // } + + // // error processing ... + // // 401-1 response + // token, ret := oauth.IsCarriedToken(r) + // if ret == false { + // log.Error("AccessToken is not carried") + // services.ResponseUnauthorized401AccessTokenNotCarried(w) + // return + // } + + se, err := dborm.XormLogoutUpdateSession(token) + if err != nil { + log.Error("Uri is invalid") + services.ResponseNotFound404UriNotExist(w, r) + return + } + // 清除缓存用户信息 + account.ClearLoginUser(se.AccountId) + services.ResponseStatusOK200Null(w) +} + +func HandshakeFromOMC(w http.ResponseWriter, r *http.Request) { + log.Info("HandshakeFromOMC processing... ") + + // check media type(content type) only support "application/json" + if !services.IsVallidContentType(r, config.GetYamlConfig().OMC.CheckContentType) { + log.Debug("Invalid Content-Type") + services.ResponseUnsupportedMediaType415(w) + return + } + + // check extend uri, response 404 + if !services.IsValidOAuthUri(r) { + log.Error("Uri is invalid") + services.ResponseNotFound404UriNotExist(w, r) + return + } + + // error processing ... + // 401-1 response + token, ret := oauth.IsCarriedToken(r) + if !ret { + log.Error("AccessToken is not carried") + services.ResponseUnauthorized401AccessTokenNotCarried(w) + return + } + + _, err := dborm.XormUpdateSessionShakeTime(token) + if err != nil { + log.Error("Uri is invalid") + services.ResponseNotFound404UriNotExist(w, r) + return + } + services.ResponseStatusOK200Null(w) +} + +// 系统登录 +// +// POST /login +func LoginOMC(w http.ResponseWriter, r *http.Request) { + log.Info("LoginOMC processing... ") + var body struct { + Username string `json:"username" binding:"required"` // Username 用户名 + Password string `json:"password" binding:"required"` // Password 用户密码 + Code string `json:"code"` // Code 验证码 + UUID string `json:"uuid"` // UUID 验证码唯一标识 + } + err := ctx.ShouldBindJSON(r, &body) + if err != nil { + log.Error("Invalid Json Format") + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + // response 400-5 + if body.Username == "" || body.Password == "" { + log.Error("Wrong parameter value") + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + // 校验验证码 + // 从数据库配置获取验证码开关 true开启,false关闭 + captchaEnabledStr := sysConfigService.NewServiceSysConfig.SelectConfigValueByKey("sys.account.captchaEnabled") + captchaEnabled, err := strconv.ParseBool(captchaEnabledStr) + if err != nil { + captchaEnabled = false + } + if captchaEnabled { + if body.Code == "" || body.UUID == "" { + log.Error("Authentication failed, mismatch captcha") + ctx.JSON(w, 400, result.CodeMsg(400, "验证码信息错误")) + return + } + verifyKey := cachekey.CAPTCHA_CODE_KEY + body.UUID + captcha, ok := cache.GetLocalTTL(verifyKey) + if captcha == nil || !ok { + log.Error("Authentication failed, captcha emtry") + ctx.JSON(w, 400, result.CodeMsg(400, "验证码已失效")) + return + } + cache.DeleteLocalTTL(verifyKey) + if captcha.(string) != body.Code { + log.Error("Authentication failed, not match captcha") + ctx.JSON(w, 400, result.CodeMsg(400, "验证码错误")) + return + } + } + + validUser, user, err := dborm.XormCheckLoginUser(body.Username, body.Password, config.GetYamlConfig().Auth.Crypt) + if !validUser || err != nil { + // response 400-4 + log.Error("Authentication failed, mismatch user or password") + ctx.JSON(w, 400, result.CodeMsg(400, err.Error())) + return + } + + token := oauth.GenRandToken("omc") // Generate new token to session ID + sourceAddr := r.RemoteAddr[:strings.Index(r.RemoteAddr, ":")] + affected, err := dborm.XormInsertSession(body.Username, sourceAddr, token, + config.GetExpiresFromConfig(), config.GetYamlConfig().Auth.Session) + if err != nil { + log.Error("Failed to XormInsertSession:", err) + if affected == -1 { + services.ResponseForbidden403MultiLoginNotAllowed(w) + } else { + services.ResponseBadRequest400IncorrectLogin(w) + } + + return + } + + if user != nil { + // 缓存用户信息 + account.CacheLoginUser(user) + ctx.JSON(w, 200, result.OkData(map[string]any{ + "accessToken": token, + })) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} + +// 获取验证码 +// +// GET /captchaImage +func CaptchaImage(w http.ResponseWriter, r *http.Request) { + configService := sysConfigService.NewServiceSysConfig + + // 从数据库配置获取验证码开关 true开启,false关闭 + captchaEnabledStr := configService.SelectConfigValueByKey("sys.account.captchaEnabled") + captchaEnabled, err := strconv.ParseBool(captchaEnabledStr) + if err != nil { + captchaEnabled = false + } + if !captchaEnabled { + ctx.JSON(w, 200, result.Ok(map[string]any{ + "captchaEnabled": captchaEnabled, + })) + return + } + + // 生成唯一标识 + verifyKey := "" + data := map[string]any{ + "captchaEnabled": captchaEnabled, + "uuid": "", + "img": "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7", + } + + // char 字符验证 + driverCaptcha := &base64Captcha.DriverString{ + //Height png height in pixel. + Height: 40, + // Width Captcha png width in pixel. + Width: 120, + //NoiseCount text noise count. + NoiseCount: 4, + //Length random string length. + Length: 4, + //Source is a unicode which is the rand string from. + Source: "023456789abcdefghjkmnprstuvwxyz", + //ShowLineOptions := OptionShowHollowLine | OptionShowSlimeLine | OptionShowSineLine . + ShowLineOptions: base64Captcha.OptionShowHollowLine, + //BgColor captcha image background color (optional) + BgColor: &color.RGBA{ + R: 250, + G: 250, + B: 250, + A: 255, // 不透明 + }, + } + // 验证码生成 + id, question, answer := driverCaptcha.GenerateIdQuestionAnswer() + // 验证码表达式解析输出 + item, err := driverCaptcha.DrawCaptcha(question) + if err != nil { + logger.Infof("Generate Id Question Answer %s : %v", question, err) + } else { + data["uuid"] = id + data["img"] = item.EncodeB64string() + verifyKey = cachekey.CAPTCHA_CODE_KEY + id + cache.SetLocalTTL(verifyKey, answer, 120*time.Second) + } + + // 本地开发下返回验证码结果,方便接口调试 + // text, ok := cache.GetLocalTTL(verifyKey) + // if ok { + // data["text"] = text.(string) + // } + + ctx.JSON(w, 200, result.Ok(data)) +} + +// 登录用户信息 +func UserInfo(w http.ResponseWriter, r *http.Request) { + loginUser, err := ctx.LoginUser(r) + if err != nil { + ctx.JSON(w, 200, result.OkData(err.Error())) + } + // 角色权限集合,管理员拥有所有权限 + userId := fmt.Sprint(loginUser.UserID) + isAdmin := conf.IsAdmin(userId) + roles, perms := service.NewServiceAccount.RoleAndMenuPerms(userId, isAdmin) + + ctx.JSON(w, 200, result.OkData(map[string]any{ + "user": loginUser.User, + "roles": roles, + "permissions": perms, + })) +} + +// 登录用户路由信息 +func Routers(w http.ResponseWriter, r *http.Request) { + userID := ctx.LoginUserToUserID(r) + + // 前端路由,管理员拥有所有 + isAdmin := conf.IsAdmin(userID) + buildMenus := service.NewServiceAccount.RouteMenus(userID, isAdmin) + ctx.JSON(w, 200, result.OkData(buildMenus)) +} diff --git a/features/security/service/service_account.go b/features/security/service/service_account.go new file mode 100644 index 0000000..ace6d82 --- /dev/null +++ b/features/security/service/service_account.go @@ -0,0 +1,56 @@ +package service + +import ( + menuService "ems.agt/features/sys_menu/service" + roleService "ems.agt/features/sys_role/service" + userService "ems.agt/features/sys_user/service" + "ems.agt/lib/core/utils/parse" + "ems.agt/lib/core/vo" +) + +// 实例化服务层 ServiceAccount 结构体 +var NewServiceAccount = &ServiceAccount{ + sysUserService: userService.NewServiceSysUser, + sysRoleService: roleService.NewServiceSysRole, + sysMenuService: menuService.NewServiceSysMenu, +} + +// 账号身份操作服务 服务层处理 +type ServiceAccount struct { + // 用户信息服务 + sysUserService *userService.ServiceSysUser + // 角色服务 + sysRoleService *roleService.ServiceSysRole + // 菜单服务 + sysMenuService *menuService.ServiceSysMenu +} + +// RoleAndMenuPerms 角色和菜单数据权限 +func (s *ServiceAccount) RoleAndMenuPerms(userId string, isAdmin bool) ([]string, []string) { + if isAdmin { + return []string{"admin"}, []string{"*:*:*"} + } else { + // 角色key + roleGroup := []string{} + roles := s.sysRoleService.SelectRoleListByUserId(userId) + for _, role := range roles { + roleGroup = append(roleGroup, role.RoleKey) + } + // 菜单权限key + perms := s.sysMenuService.SelectMenuPermsByUserId(userId) + return parse.RemoveDuplicates(roleGroup), parse.RemoveDuplicates(perms) + } +} + +// RouteMenus 前端路由所需要的菜单 +func (s *ServiceAccount) RouteMenus(userId string, isAdmin bool) []vo.Router { + var buildMenus []vo.Router + if isAdmin { + menus := s.sysMenuService.SelectMenuTreeByUserId("*") + buildMenus = s.sysMenuService.BuildRouteMenus(menus, "") + } else { + menus := s.sysMenuService.SelectMenuTreeByUserId(userId) + buildMenus = s.sysMenuService.BuildRouteMenus(menus, "") + } + return buildMenus +} diff --git a/features/sm/backup.go b/features/sm/backup.go new file mode 100644 index 0000000..ffb7051 --- /dev/null +++ b/features/sm/backup.go @@ -0,0 +1,109 @@ +package sm + +import ( + "database/sql" + "fmt" + "os" + "os/exec" + "time" + + "ems.agt/lib/log" + "ems.agt/restagent/config" + _ "github.com/go-sql-driver/mysql" +) + +var dbConfig = config.GetYamlConfig().Database + +func DatabaseWhoreBackup() { + // MySQL数据库连接信息 + sqlStr := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=true&loc=Local", + dbConfig.User, dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Name) + db, err := sql.Open("mysql", sqlStr) + if err != nil { + log.Error("Failed to connect to database:", err) + return + } + defer db.Close() + + // 备份SQL文件路径 + backupFile := dbConfig.Backup + "/" + "whore_backup_" + dbConfig.Name + ".sql" + + // 执行mysqldump命令进行备份 + cmd := exec.Command("mysqldump", "-u", dbConfig.User, "-p"+dbConfig.Password, "-h", dbConfig.Host, dbConfig.Name) + output, err := cmd.Output() + if err != nil { + log.Error("Failed to execute mysqldump command:", err) + return + } + + // 将备份结果写入SQL文件 + file, err := os.Create(backupFile) + if err != nil { + log.Error("Failed to create backup file:", err) + return + } + defer file.Close() + + _, err = file.Write(output) + if err != nil { + log.Error("Failed to write backup file:", err) + return + } + + log.Info("Backup completed successfully.") +} + +func DatabaseIncrementalBackup() { + // MySQL数据库连接信息 + sqlStr := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=true&loc=Local", + dbConfig.User, dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Name) + db, err := sql.Open("mysql", sqlStr) + if err != nil { + log.Error("Failed to connect to database:", err) + return + } + defer db.Close() + + // 备份SQL文件路径 + backupFile := dbConfig.Backup + "/" + "incremental_backup_" + dbConfig.Name + ".sql" + + // 上次备份的时间点 + lastBackupTime := time.Date(2022, time.January, 1, 0, 0, 0, 0, time.Local) + + // 构建增量备份SQL语句 + query := fmt.Sprintf("SELECT * FROM table WHERE modified_at > '%s'", lastBackupTime.Format("2006-01-02 15:04:05")) + + // 执行查询 + rows, err := db.Query(query) + if err != nil { + log.Error("Failed to execute query:", err) + return + } + defer rows.Close() + + // 创建增量备份SQL文件 + file, err := os.Create(backupFile) + if err != nil { + log.Error("Failed to create backup file:", err) + return + } + defer file.Close() + + // 将查询结果写入SQL文件 + for rows.Next() { + var data string + err := rows.Scan(&data) + if err != nil { + log.Error("Failed to scan row:", err) + return + } + + _, err = file.WriteString(data + "\n") + if err != nil { + log.Error("Failed to write backup file:", err) + return + } + } + + log.Info("Incremental backup completed successfully.") +} diff --git a/features/state/getstate.go b/features/state/getstate.go new file mode 100644 index 0000000..bcffc13 --- /dev/null +++ b/features/state/getstate.go @@ -0,0 +1,852 @@ +package state + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "strconv" + "strings" + "time" + + "github.com/go-resty/resty/v2" + "github.com/gorilla/mux" + + "ems.agt/lib/dborm" + "ems.agt/lib/global" + "ems.agt/lib/log" + "ems.agt/lib/services" + "ems.agt/restagent/config" +) + +type CpuUsage struct { + NfCpuUsage uint16 `json:"nfCpuUsage"` + SysCpuUsage uint16 `json:"sysCpuUsage"` +} + +type MemUsage struct { + TotalMem uint32 `json:"totalMem"` + NfUsedMem uint32 `json:"nfUsedMem"` + SysMemUsage uint16 `json:"sysMemUsage"` +} + +type PartitionInfo struct { + Total uint32 `json:"total"` // MB + Used uint32 `json:"used"` // MB +} + +type DiskSpace struct { + PartitionNum uint8 `json:"partitionNum"` + PartitionInfo []PartitionInfo `json:"partitionInfo"` +} + +type HardwareInfo struct { + CPUs int `json:"cpus"` + Memory int `json:"memory"` +} + +type SysState struct { + HostName string `json:"hostName"` // linux命令: hostname + OsInfo string `json:"osInfo"` // linux命令: uname -a + DbInfo string `json:"dbInfo"` // 网元如果有db, 显示数据库名和版本信息, OMC: mysql --version + Version string `json:"version"` // 软件版本信息: 16.1.1 + IpAddr []string `json:"ipAddr"` // 网管的ipv4和ipv6列表 + Port uint16 `json:"port"` // 用于网管的port + Capability uint32 `json:"capability"` + SerialNum string `json:"serialNum"` + ExpiryDate string `json:"expiryDate"` + HardwareInfo HardwareInfo `json:"hardwareInfo"` + CpuUsage CpuUsage `json:"cpuUsage"` + MemUsage MemUsage `json:"memUsage"` + DiskSpace DiskSpace `json:"diskSpace"` + //Timestamp string `json:"timestamp"` +} + +type SystemState struct { + HostName string `json:"hostName"` // linux命令: hostname + OsInfo string `json:"osInfo"` // linux命令: uname -a + DbInfo string `json:"dbInfo"` // 网元如果有db, 显示数据库名和版本信息, OMC: mysql --version + Version string `json:"version"` // 软件版本信息: 16.1.1 + IpAddr []string `json:"ipAddr"` // 网管的ipv4和ipv6列表 + Port uint16 `json:"port"` // 用于网管的port + Capability uint32 `json:"capability"` + SerialNum string `json:"serialNum"` + ExpiryDate string `json:"expiryDate"` + HardwareInfo struct { + CPUs int `json:"cpus"` // 主机(裸机/虚拟机)的cpu个数 + Memory int `json:"memory"` // 主机(裸机/虚拟机): 配置的内存 + } `json:"hardwareInfo"` + CpuUsage struct { + NfCpuUsage uint16 `json:"nfCpuUsage"` + SysCpuUsage uint16 `json:"sysCpuUsage"` + } `json:"cpuUsage"` + MemUsage struct { + TotalMem uint32 `json:"totalMem"` + NfUsedMem uint32 `json:"nfUsedMem"` + SysMemUsage uint16 `json:"sysMemUsage"` + } `json:"memUsage"` + DiskSpace struct { + PartitionNum uint8 `json:"partitionNum"` + PartitionInfo []struct { + Total uint32 `json:"total"` // MB + Used uint32 `json:"used"` // MB + } `json:"partitionInfo"` + } `json:"diskSpace"` + //Timestamp string `json:"timestamp"` +} + +type SystemInfo struct { + NeType string `json:"neType" map:"neType, omitempty"` + NeId string `json:"neId" map:"neId, omitempty"` + HostName string `json:"hostName" map:"hostName, omitempty"` // linux命令: hostname + OsInfo string `json:"osInfo" map:"osInfo, omitempty"` // linux命令: uname -a + DbInfo string `json:"dbInfo" map:"dbInfo, omitempty"` // 网元如果有db, 显示数据库名和版本信息, OMC: mysql --version + Version string `json:"version" map:"version, omitempty"` // 软件版本信息: 16.1.1 + IpAddr string `json:"ipAddr" map:"ipAddr, omitempty"` // 网管的ipv4和ipv6列表 + Port uint16 `json:"port" map:"port, omitempty"` // 用于网管的port + CPUs int `json:"cpus" map:"cpus, omitempty"` + TotalMem int `json:"totalMem" map:"totalMem, omitempty"` + PvFlag string `json:"pvFlag" map:"pvFlag, omitempty"` + Status string `json:"status" map:"status, omitempty"` +} + +type LicenseInfo struct { + NeType string `json:"neType"` + NeId string `json:"neId"` + SerialNum string `json:"serialNum"` + Capability uint32 `json:"capability"` + CapUsed uint32 `json:"capUsed"` + FeatureEnabled []string `json:"featureEnabled"` + ExpiryDate string `json:"expiryDate"` +} + +type Response struct { + Data interface{} `json:"data"` +} + +var ( + UriSysState = config.DefaultUriPrefix + "/systemManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/systemState" + UriSysState2 = config.DefaultUriPrefix + "/systemManagement/{apiVersion}/{elementTypeValue}/systemState" + UriSysInfoAll = config.DefaultUriPrefix + "/systemManagement/{apiVersion}/sysInfo" + UriSysInfoOne = config.DefaultUriPrefix + "/systemManagement/{apiVersion}/sysInfo/{neType}/{neId}" + UriLicenseInfoAll = config.DefaultUriPrefix + "/systemManagement/{apiVersion}/licenseInfo" + UriLicenseInfoOne = config.DefaultUriPrefix + "/systemManagement/{apiVersion}/licenseInfo/{neType}/{neId}" + + CustomUriSysState = config.UriPrefix + "/systemManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/systemState" + CustomUriSysState2 = config.UriPrefix + "/systemManagement/{apiVersion}/{elementTypeValue}/systemState" + CustomUriSysInfoAll = config.UriPrefix + "/systemManagement/{apiVersion}/sysInfo" + CustomUriSysInfoOne = config.UriPrefix + "/systemManagement/{apiVersion}/sysInfo/{neType}/{neId}" + CustomUriLicenseInfoAll = config.UriPrefix + "/systemManagement/{apiVersion}/licenseInfo" + CustomUriLicenseInfoOne = config.UriPrefix + "/systemManagement/{apiVersion}/licenseInfo/{neType}/{neId}" +) + +var client = resty.New() + +func init() { + /* + client. + SetTimeout(10 * time.Second). + SetRetryCount(1). + SetRetryWaitTime(1 * time.Second). + SetRetryMaxWaitTime(2 * time.Second). + SetRetryAfter(func(client *resty.Client, resp *resty.Response) (time.Duration, error) { + return 0, errors.New("quota exceeded") + }) + */ + client.SetTimeout(3 * time.Second) +} + +func NeStatusEnumToStr(intStatus int) string { + switch intStatus { + case 0: + return "active" + case 1: + return "offline" + case 2: + return "standby" + case 3: + return "maintain" + default: + return "unkown" + } +} + +// Get system state from NF/NFs +func GetOneLicenseInfoFromNF(w http.ResponseWriter, r *http.Request) { + log.Debug("GetOneLicenseInfoFromNF processing... ") + + data := make([]map[string]interface{}, 0) + + vars := mux.Vars(r) + neType := vars["neType"] + neId := vars["neId"] + if neType == "" || neId == "" { + services.ResponseNotFound404UriNotExist(w, r) + return + } + token, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + log.Debug("AccessToken:", token) + + neInfo, err := dborm.XormGetNeInfo(neType, neId) + if err != nil { + log.Error("Failed to XormGetNeInfo:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } else if neInfo == nil { + err := global.ErrCMNotFoundTargetNE + log.Error(global.ErrCMNotFoundTargetNE) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + log.Trace("neInfo:", neInfo) + + //systemState := make(map[string]interface{}) + systemState := &SysState{} + result := make(map[string]interface{}) + //sysInfo := &SystemInfo{} + omcNeTypeLower := "omc" + if config.GetYamlConfig().OMC.NeType != "" { + omcNeTypeLower = strings.ToLower(config.GetYamlConfig().OMC.NeType) + } + if neType != omcNeTypeLower { + log.Debugf("r.RemoteAddr: %s omcNeTypeLower: %s", r.RemoteAddr, omcNeTypeLower) + var requestURI2NF string + if config.GetYamlConfig().OMC.TestMode == true && strings.ToLower(neType) != "udm" { + var udmNEs []dborm.NeInfo + err := dborm.XormGetNeInfoByNeType("UDM", &udmNEs) + if err != nil { + log.Error("Get system state from NF is failed:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + if len(udmNEs) > 0 { + udmNe := udmNEs[0] + hostUri := fmt.Sprintf("http://%s:%v", udmNe.Ip, udmNe.Port) + requestURI2NF = fmt.Sprintf("%s/api/rest/systemManagement/v1/elementType/%s/objectType/systemState", + hostUri, strings.ToLower(udmNe.NeType)) + } + } else { + hostUri := fmt.Sprintf("http://%s:%v", neInfo.Ip, neInfo.Port) + requestURI2NF = fmt.Sprintf("%s/api/rest/systemManagement/v1/elementType/%s/objectType/systemState", + hostUri, strings.ToLower(neInfo.NeType)) + } + log.Debug("requestURI2NF:", requestURI2NF) + + resp, err := client.R(). + EnableTrace(). + SetHeaders(map[string]string{"accessToken": token}). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + Get(requestURI2NF) + if err != nil { + log.Error("Get system state from NF is failed:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } else { + log.Trace("resp.Body():", string(resp.Body())) + _ = json.Unmarshal(resp.Body(), &systemState) + + log.Trace("systemState:", systemState) + capUsed := config.TDatas[neInfo.NeType].CapUsed + log.Tracef("neInfo.NeType:%s capUsed: %v", capUsed) + licenseInfo := &LicenseInfo{ + NeType: neInfo.NeType, + NeId: neInfo.NeId, + SerialNum: systemState.SerialNum, + Capability: systemState.Capability, + CapUsed: capUsed, + FeatureEnabled: config.TDatas[neInfo.NeType].FeatureEnabled, + ExpiryDate: systemState.ExpiryDate, + } + //neItem := strings.ToUpper(neType) + "/" + neId + result, err = global.ToMap(*licenseInfo, "json") + } + } else { + systemState := GetEMSState(neInfo.Ip) + licenseInfo := &LicenseInfo{ + NeType: neInfo.NeType, + NeId: neInfo.NeId, + SerialNum: systemState.SerialNum, + Capability: systemState.Capability, + CapUsed: config.TDatas[neInfo.NeType].CapUsed, + FeatureEnabled: config.TDatas[neInfo.NeType].FeatureEnabled, + ExpiryDate: systemState.ExpiryDate, + } + result, err = global.ToMap(*licenseInfo, "json") + // neItem := strings.ToUpper(neType) + "/" + neId + // result[neItem] = sysInfo + } + + data = append(data, result) + log.Trace("data:", data) + + var response Response + response.Data = data + services.ResponseWithJson(w, http.StatusOK, response) +} + +// Get system state from NF/NFs +func GetAllLicenseInfoFromNF(w http.ResponseWriter, r *http.Request) { + log.Debug("GetAllLicenseInfoFromNF processing... ") + + data := make([]map[string]interface{}, 0) + + token, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + log.Debug("AccessToken:", token) + + var neList []dborm.NeInfo + _, err = dborm.XormGetAllNeInfo(&neList) + omcNeTypeLower := "omc" + if config.GetYamlConfig().OMC.NeType != "" { + omcNeTypeLower = strings.ToLower(config.GetYamlConfig().OMC.NeType) + } + + for _, ne := range neList { + result := make(map[string]interface{}) + log.Debugf("r.RemoteAddr: %s omcNeTypeLower: %s", r.RemoteAddr, omcNeTypeLower) + log.Debug("ne: ", ne) + //if strings.ToLower(ne.NeType) != omcNeTypeLower || !strings.Contains(r.RemoteAddr, ne.Ip) { + if strings.ToLower(ne.NeType) != omcNeTypeLower { + // hostUri := fmt.Sprintf("http://%s:%v", ne.Ip, ne.Port) + // requestURI2NF := fmt.Sprintf("%s/api/rest/systemManagement/v1/elementType/%s/objectType/systemState", + // hostUri, strings.ToLower(ne.NeType)) + var requestURI2NF string + if config.GetYamlConfig().OMC.TestMode == true && strings.ToLower(ne.NeType) != "udm" { + var udmNEs []dborm.NeInfo + err := dborm.XormGetNeInfoByNeType("UDM", &udmNEs) + if err != nil { + log.Error("Get system state from NF is failed:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + if len(udmNEs) > 0 { + udmNe := udmNEs[0] + hostUri := fmt.Sprintf("http://%s:%v", udmNe.Ip, udmNe.Port) + requestURI2NF = fmt.Sprintf("%s/api/rest/systemManagement/v1/elementType/%s/objectType/systemState", + hostUri, strings.ToLower(udmNe.NeType)) + } + } else { + hostUri := fmt.Sprintf("http://%s:%v", ne.Ip, ne.Port) + requestURI2NF = fmt.Sprintf("%s/api/rest/systemManagement/v1/elementType/%s/objectType/systemState", + hostUri, strings.ToLower(ne.NeType)) + } + log.Debug("requestURI2NF:", requestURI2NF) + + resp, err := client.R(). + EnableTrace(). + SetHeaders(map[string]string{"accessToken": token}). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + Get(requestURI2NF) + if err != nil { + log.Error("Get system state from NF is failed:", err) + // errorMessage := services.ErrorMessage{ + // ErrorCode: "1", ErrorInfo: "Internal server error, NF connnect refused", + // } + // //result["error"] = errorMessage + continue + } else { + systemState := &SysState{} + _ = json.Unmarshal(resp.Body(), &systemState) + licenseInfo := &LicenseInfo{ + NeType: ne.NeType, + NeId: ne.NeId, + SerialNum: systemState.SerialNum, + Capability: systemState.Capability, + CapUsed: config.TDatas[ne.NeType].CapUsed, + FeatureEnabled: config.TDatas[ne.NeType].FeatureEnabled, + ExpiryDate: systemState.ExpiryDate, + } + result, err = global.ToMap(*licenseInfo, "json") + // neItem := strings.ToUpper(ne.NeType) + "/" + ne.NeId + // result[neItem] = sysInfo + } + } else { + systemState := GetEMSState(ne.Ip) + licenseInfo := &LicenseInfo{ + NeType: ne.NeType, + NeId: ne.NeId, + SerialNum: systemState.SerialNum, + Capability: systemState.Capability, + CapUsed: config.TDatas[ne.NeType].CapUsed, + FeatureEnabled: config.TDatas[ne.NeType].FeatureEnabled, + ExpiryDate: systemState.ExpiryDate, + } + result, err = global.ToMap(*licenseInfo, "json") + // neItem := strings.ToUpper(ne.NeType) + "/" + ne.NeId + // result[neItem] = sysInfo + } + + data = append(data, result) + log.Trace("data:", data) + } + + var response Response + response.Data = data + services.ResponseWithJson(w, http.StatusOK, response) +} + +// Get system state from NF/NFs +func GetOneSysinfoFromNF(w http.ResponseWriter, r *http.Request) { + log.Debug("GetOneSysinfoFromNF processing... ") + + data := make([]map[string]interface{}, 0) + + vars := mux.Vars(r) + neType := vars["neType"] + neId := vars["neId"] + if neType == "" || neId == "" { + services.ResponseNotFound404UriNotExist(w, r) + return + } + token, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + log.Debug("AccessToken:", token) + + neInfo, err := dborm.XormGetNeInfo(neType, neId) + if err != nil { + log.Error("Failed to XormGetNeInfo:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } else if neInfo == nil { + err := global.ErrCMNotFoundTargetNE + log.Error(global.ErrCMNotFoundTargetNE) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + log.Trace("neInfo:", neInfo) + + //systemState := make(map[string]interface{}) + systemState := &SysState{} + result := make(map[string]interface{}) + //sysInfo := &SystemInfo{} + omcNeTypeLower := "omc" + if config.GetYamlConfig().OMC.NeType != "" { + omcNeTypeLower = strings.ToLower(config.GetYamlConfig().OMC.NeType) + } + if neType != omcNeTypeLower { + log.Debugf("r.RemoteAddr: %s omcNeTypeLower: %s", r.RemoteAddr, omcNeTypeLower) + var requestURI2NF string + if config.GetYamlConfig().OMC.TestMode == true && strings.ToLower(neType) != "udm" { + var udmNEs []dborm.NeInfo + err := dborm.XormGetNeInfoByNeType("UDM", &udmNEs) + if err != nil { + log.Error("Get system state from NF is failed:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + if len(udmNEs) > 0 { + udmNe := udmNEs[0] + hostUri := fmt.Sprintf("http://%s:%v", udmNe.Ip, udmNe.Port) + requestURI2NF = fmt.Sprintf("%s/api/rest/systemManagement/v1/elementType/%s/objectType/systemState", + hostUri, strings.ToLower(udmNe.NeType)) + } + } else { + hostUri := fmt.Sprintf("http://%s:%v", neInfo.Ip, neInfo.Port) + requestURI2NF = fmt.Sprintf("%s/api/rest/systemManagement/v1/elementType/%s/objectType/systemState", + hostUri, strings.ToLower(neInfo.NeType)) + } + log.Debug("requestURI2NF:", requestURI2NF) + + resp, err := client.R(). + EnableTrace(). + SetHeaders(map[string]string{"accessToken": token}). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + Get(requestURI2NF) + if err != nil { + log.Error("Get system state from NF is failed:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } else { + log.Trace("resp.Body():", string(resp.Body())) + _ = json.Unmarshal(resp.Body(), &systemState) + + log.Trace("systemState:", systemState) + hostName := "5gc" + if systemState.HostName != "" { + hostName = systemState.HostName + } + osInfo := "Linux 5gc 4.15.0-29-generic #31-Ubuntu SMP Tue Jul 17 15:39:52 UTC 2018 x86_64 GNU/Linux" + if systemState.OsInfo != "" { + osInfo = systemState.OsInfo + } + dbInfo := "adb v1.0.1" + if systemState.OsInfo != "" { + dbInfo = systemState.DbInfo + } + port, _ := strconv.Atoi(neInfo.Port) + cpus := 4 + if systemState.HardwareInfo.CPUs != 0 { + cpus = systemState.HardwareInfo.CPUs + } + totalMem := 34029125632 + if systemState.HardwareInfo.Memory != 0 { + totalMem = systemState.HardwareInfo.Memory + } + sysInfo := &SystemInfo{ + NeType: neInfo.NeType, + NeId: neInfo.NeId, + HostName: hostName, + OsInfo: osInfo, + DbInfo: dbInfo, + Version: systemState.Version, + IpAddr: neInfo.Ip, + Port: uint16(port), + CPUs: cpus, + TotalMem: totalMem, + PvFlag: neInfo.PvFlag, + Status: NeStatusEnumToStr(neInfo.Status), + } + //neItem := strings.ToUpper(neType) + "/" + neId + result, err = global.ToMap(*sysInfo, "json") + } + } else { + systemState := GetEMSState(neInfo.Ip) + sysInfo := &SystemInfo{ + NeType: neInfo.NeType, + NeId: neInfo.NeId, + HostName: systemState.HostName, + OsInfo: systemState.OsInfo, + DbInfo: systemState.DbInfo, + Version: systemState.Version, + IpAddr: neInfo.Ip, + Port: systemState.Port, + CPUs: systemState.HardwareInfo.CPUs, + TotalMem: systemState.HardwareInfo.Memory, + PvFlag: neInfo.PvFlag, + Status: NeStatusEnumToStr(neInfo.Status), + } + result, err = global.ToMap(*sysInfo, "json") + // neItem := strings.ToUpper(neType) + "/" + neId + // result[neItem] = sysInfo + } + + data = append(data, result) + log.Trace("data:", data) + + var response Response + response.Data = data + services.ResponseWithJson(w, http.StatusOK, response) +} + +// Get system state from NF/NFs +func GetAllSysinfoFromNF(w http.ResponseWriter, r *http.Request) { + log.Debug("GetAllSysinfoFromNF processing... ") + + data := make([]map[string]interface{}, 0) + + token, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + log.Debug("AccessToken:", token) + + var neList []dborm.NeInfo + _, err = dborm.XormGetAllNeInfo(&neList) + omcNeTypeLower := "omc" + if config.GetYamlConfig().OMC.NeType != "" { + omcNeTypeLower = strings.ToLower(config.GetYamlConfig().OMC.NeType) + } + + for _, ne := range neList { + result := make(map[string]interface{}) + log.Debugf("r.RemoteAddr: %s omcNeTypeLower: %s", r.RemoteAddr, omcNeTypeLower) + log.Debug("ne: ", ne) + //if strings.ToLower(ne.NeType) != omcNeTypeLower || !strings.Contains(r.RemoteAddr, ne.Ip) { + if strings.ToLower(ne.NeType) != omcNeTypeLower { + // hostUri := fmt.Sprintf("http://%s:%v", ne.Ip, ne.Port) + // requestURI2NF := fmt.Sprintf("%s/api/rest/systemManagement/v1/elementType/%s/objectType/systemState", + // hostUri, strings.ToLower(ne.NeType)) + var requestURI2NF string + if config.GetYamlConfig().OMC.TestMode == true && strings.ToLower(ne.NeType) != "udm" { + var udmNEs []dborm.NeInfo + err := dborm.XormGetNeInfoByNeType("UDM", &udmNEs) + if err != nil { + log.Error("Get system state from NF is failed:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + if len(udmNEs) > 0 { + udmNe := udmNEs[0] + hostUri := fmt.Sprintf("http://%s:%v", udmNe.Ip, udmNe.Port) + requestURI2NF = fmt.Sprintf("%s/api/rest/systemManagement/v1/elementType/%s/objectType/systemState", + hostUri, strings.ToLower(udmNe.NeType)) + } + } else { + hostUri := fmt.Sprintf("http://%s:%v", ne.Ip, ne.Port) + requestURI2NF = fmt.Sprintf("%s/api/rest/systemManagement/v1/elementType/%s/objectType/systemState", + hostUri, strings.ToLower(ne.NeType)) + } + log.Debug("requestURI2NF:", requestURI2NF) + + resp, err := client.R(). + EnableTrace(). + SetHeaders(map[string]string{"accessToken": token}). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + Get(requestURI2NF) + if err != nil { + log.Error("Get system state from NF is failed:", err) + // errorMessage := services.ErrorMessage{ + // ErrorCode: "1", ErrorInfo: "Internal server error, NF connnect refused", + // } + // //result["error"] = errorMessage + continue + } else { + systemState := &SysState{} + _ = json.Unmarshal(resp.Body(), &systemState) + hostName := "5gc" + if systemState.HostName != "" { + hostName = systemState.HostName + } + osInfo := "Linux 5gc 4.15.0-29-generic #31-Ubuntu SMP Tue Jul 17 15:39:52 UTC 2018 x86_64 GNU/Linux" + if systemState.OsInfo != "" { + osInfo = systemState.OsInfo + } + dbInfo := "adb v1.0.1" + if systemState.OsInfo != "" { + dbInfo = systemState.DbInfo + } + port, _ := strconv.Atoi(ne.Port) + cpus := 4 + if systemState.HardwareInfo.CPUs != 0 { + cpus = systemState.HardwareInfo.CPUs + } + totalMem := 34029125632 + if systemState.HardwareInfo.Memory != 0 { + totalMem = systemState.HardwareInfo.Memory + } + sysInfo := &SystemInfo{ + NeType: ne.NeType, + NeId: ne.NeId, + HostName: hostName, + OsInfo: osInfo, + DbInfo: dbInfo, + Version: systemState.Version, + IpAddr: ne.Ip, + Port: uint16(port), + CPUs: cpus, + TotalMem: totalMem, + PvFlag: ne.PvFlag, + Status: NeStatusEnumToStr(ne.Status), + } + // neItem := strings.ToUpper(ne.NeType) + "/" + ne.NeId + // result[neItem] = sysInfo + result, err = global.ToMap(*sysInfo, "json") + } + } else { + port, _ := strconv.Atoi(ne.Port) + systemState := GetEMSState(ne.Ip) + sysInfo := &SystemInfo{ + NeType: ne.NeType, + NeId: ne.NeId, + HostName: systemState.HostName, + OsInfo: systemState.OsInfo, + DbInfo: systemState.DbInfo, + Version: systemState.Version, + IpAddr: ne.Ip, + Port: (uint16(port)), + CPUs: systemState.HardwareInfo.CPUs, + TotalMem: systemState.HardwareInfo.Memory, + PvFlag: ne.PvFlag, + Status: NeStatusEnumToStr(ne.Status), + } + // neItem := strings.ToUpper(ne.NeType) + "/" + ne.NeId + // result[neItem] = sysInfo + result, err = global.ToMap(*sysInfo, "json") + } + + data = append(data, result) + log.Trace("data:", data) + } + + var response Response + response.Data = data + services.ResponseWithJson(w, http.StatusOK, response) +} + +// Get system state from NF/NFs +func GetStateFromNF(w http.ResponseWriter, r *http.Request) { + log.Debug("GetStateFromNF processing... ") + + data := make([]map[string]interface{}, 0) + + vars := mux.Vars(r) + neType := vars["elementTypeValue"] + + var neList []dborm.NeInfo + if neType == "" { + services.ResponseNotFound404UriNotExist(w, r) + return + } + token, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + log.Debug("AccessToken:", token) + + switch strings.ToLower(neType) { + case "all": + // query all NFs + // create rest client + restHostPort := fmt.Sprintf("http://127.0.0.1:%d", config.GetYamlConfig().Rest[0].Port) + getNeInfoPattern := fmt.Sprintf(config.UriPrefix+"/databaseManagement/v1/elementType/%s/objectType/ne_info", + config.GetYamlConfig().Database.Name) + getNeInfoURI := restHostPort + getNeInfoPattern + "?WHERE=status='0'" + log.Debug("getNeInfoPattern:", getNeInfoPattern) + + resp, err := client.R(). + EnableTrace(). + SetHeaders(map[string]string{"AccessToken": token}). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + Get(getNeInfoURI) + if err != nil { + log.Error("Get ne_info from DB is failed:", err) + services.ResponseInternalServerError500NFConnectRefused(w) + return + } + + neList, _ = dborm.XormParseResult(resp.Body()) + default: + restHostPort := fmt.Sprintf("http://127.0.0.1:%d", config.GetYamlConfig().Rest[0].Port) + getNeInfoPattern := fmt.Sprintf(config.UriPrefix+"/databaseManagement/v1/elementType/%s/objectType/ne_info", + config.GetYamlConfig().Database.Name) + getNeInfoURI := restHostPort + getNeInfoPattern + neId := services.GetUriParamString(r, "ne_id", ",", true, false) + if neId == "" { + getNeInfoURI = getNeInfoURI + fmt.Sprintf("?WHERE=status='0'+and+ne_type='%s'", neType) + } else { + getNeInfoURI = getNeInfoURI + fmt.Sprintf("?WHERE=status='0'+and+ne_type='%v'+and+ne_id+in+%v", neType, neId) + } + log.Debug("getNeInfoURI:", getNeInfoURI) + + resp, err := client.R(). + EnableTrace(). + SetHeaders(map[string]string{"accessToken": token}). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + Get(getNeInfoURI) + if err != nil { + log.Error("Get ne_info from DB is failed:", err) + services.ResponseInternalServerError500NFConnectRefused(w) + return + } + + neList, _ = dborm.XormParseResult(resp.Body()) + } + omcNeTypeLower := "omc" + if config.GetYamlConfig().OMC.NeType != "" { + omcNeTypeLower = strings.ToLower(config.GetYamlConfig().OMC.NeType) + } + for _, ne := range neList { + result := make(map[string]interface{}) + log.Debugf("r.RemoteAddr: %s omcNeTypeLower: %s", r.RemoteAddr, omcNeTypeLower) + log.Debug("ne: ", ne) + //if strings.ToLower(ne.NeType) != omcNeTypeLower || !strings.Contains(r.RemoteAddr, ne.Ip) { + if strings.ToLower(ne.NeType) != omcNeTypeLower { + hostUri := fmt.Sprintf("http://%s:%v", ne.Ip, ne.Port) + requestURI2NF := fmt.Sprintf("%s/api/rest/systemManagement/v1/elementType/%s/objectType/systemState", + hostUri, ne.NeType) + log.Debug("requestURI2NF:", requestURI2NF) + + result["ipAddress"] = ne.Ip + resp, err := client.R(). + EnableTrace(). + SetHeaders(map[string]string{"accessToken": token}). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + Get(requestURI2NF) + if err != nil { + log.Error("Get system state from NF is failed:", err) + errorMessage := services.ErrorMessage{ + ErrorCode: "1", ErrorInfo: "Internal server error, NF connnect refused", + } + result["error"] = errorMessage + } else { + systemState := make(map[string]interface{}) + _ = json.Unmarshal(resp.Body(), &systemState) + result["systemState"] = systemState + } + } else { + result["ipAddress"] = ne.Ip + emsState := GetEMSState(ne.Ip) + result["systemState"] = emsState + } + neItem := strings.ToUpper(ne.NeType) + "/" + ne.NeId + mapState := make(map[string]interface{}) + mapState[neItem] = result + + data = append(data, mapState) + log.Trace("data:", data) + } + + var response Response + response.Data = data + services.ResponseWithJson(w, http.StatusOK, response) +} + +func GetEMSState(ip string) *SysState { + log.Debug("GetEMSState processing... ") + + sysInfo := new(SysInfo) + err := GetSysInfo(sysInfo) + if err != nil { + log.Error("Failed to GetSysInfo:", err) + return nil + } + + cpuUsage := &CpuUsage{ + NfCpuUsage: sysInfo.MyCpuPercent, + SysCpuUsage: sysInfo.SysCpuPercent, + } + + memUsage := &MemUsage{ + TotalMem: sysInfo.SysTotalRam, + NfUsedMem: sysInfo.MyUsedRam, + SysMemUsage: sysInfo.SysRamUsedPercent, + } + + diskSpace := &DiskSpace{ + PartitionNum: sysInfo.PartitionNum, + PartitionInfo: sysInfo.PartitionInfo, + } + + version := "16.1.1" + if global.Version != "" { + version = global.Version + } + hostName, _ := os.Hostname() + emsState := &SysState{ + HostName: hostName, + OsInfo: getUnameStr(), + DbInfo: "mysql Ver 15.1 Distrib 10.3.35-MariaDB, for Linux (aarch64) using readline 5.1", + IpAddr: []string{ip}, + Port: 3030, + Version: version, + Capability: 9999999, + SerialNum: config.GetYamlConfig().OMC.Sn, + ExpiryDate: "-", + HardwareInfo: HardwareInfo{CPUs: getCpuNumber(), Memory: getTotalMemory()}, + CpuUsage: *cpuUsage, + MemUsage: *memUsage, + DiskSpace: *diskSpace, + } + + //getSystemInfo() + return emsState +} diff --git a/features/state/state_linux.go b/features/state/state_linux.go new file mode 100644 index 0000000..2c9a330 --- /dev/null +++ b/features/state/state_linux.go @@ -0,0 +1,243 @@ +//go:build linux +// +build linux + +package state + +import ( + "encoding/binary" + "fmt" + "os" + "runtime" + "syscall" + "time" + + "ems.agt/lib/log" + "github.com/shirou/gopsutil/cpu" + "github.com/shirou/gopsutil/disk" + "github.com/shirou/gopsutil/mem" + "github.com/shirou/gopsutil/process" +) + +type SysInfo struct { + SysCpuPercent uint16 // x% + MyCpuPercent uint16 // x%, CPU percent of current proccess + + SysTotalRam uint32 // KB + MyUsedRam uint32 // RAM usage of current proccess, KB + SysRamUsedPercent uint16 // x% + + PartitionNum byte + PartitionInfo []PartitionInfo // usage of each partition +} + +const MAX_PARTITION_NUM byte = 32 + +func GetSysStat() ([]byte, int) { + // Get sys info + var sysInfo SysInfo + err := GetSysInfo(&sysInfo) + if err != nil { + return nil, 0 + } + + //log.Tracef("current sys info: %v", sysInfo) + + // build ems buffer + var data []byte = make([]byte, 1024) + var len int + var i byte + binary.BigEndian.PutUint16(data[0:], sysInfo.MyCpuPercent) //x% * 100 + binary.BigEndian.PutUint16(data[2:], sysInfo.SysCpuPercent) //x% * 100 + binary.BigEndian.PutUint32(data[4:], sysInfo.SysTotalRam) // KB + binary.BigEndian.PutUint32(data[8:], sysInfo.MyUsedRam) // KB + binary.BigEndian.PutUint16(data[12:], sysInfo.SysRamUsedPercent) //x% * 100 + data[14] = sysInfo.PartitionNum + for i = 0; i < sysInfo.PartitionNum; i++ { + binary.BigEndian.PutUint32(data[15+8*(i):], sysInfo.PartitionInfo[i].Total) // MB + binary.BigEndian.PutUint32(data[15+8*(i)+4:], sysInfo.PartitionInfo[i].Used) // MB + } + len = int(15 + 8*sysInfo.PartitionNum) + + //log.Tracef("current sys stat buf: %v, len: %d", data, len) + + return data, len +} + +var pProc *process.Process = nil + +func GetSYsCpuPercent() float64 { + totalPercent, err := cpu.Percent(0, false) //(2*time.Second, false) + if err != nil { + return 0.0 + } else { + return totalPercent[0] + } +} + +func GetSysInfo(sysInfo *SysInfo) error { + // sys cpu percent + totalPercent, err := cpu.Percent(0, false) //(2*time.Second, false) + if err != nil { + sysInfo.SysCpuPercent = 0 + } else { + sysInfo.SysCpuPercent = uint16(totalPercent[0] * 100) + } + + if pProc == nil { + checkPid := os.Getpid() + pProc, err = process.NewProcess(int32(checkPid)) + if err != nil { + log.Tracef("get process info error %v", err) + return err + } + } + + // self cpu percent + percent, err := pProc.Percent(0) //(2*time.Second) + if err != nil { + log.Tracef("get process cpu percent error %v", err) + sysInfo.MyCpuPercent = 0 + } else { + sysInfo.MyCpuPercent = uint16(percent * 100) + } + + // self RAM(KB) + myRam, err := pProc.MemoryInfo() + if err != nil { + log.Tracef("get self memory info error %v", err) + sysInfo.MyUsedRam = 0 + } else { + sysInfo.MyUsedRam = uint32(myRam.RSS / 1024) + } + + // system RAM(KB) + sysRam, err := mem.VirtualMemory() + if err != nil { + log.Tracef("gett sys memory info error %v", err) + sysInfo.SysTotalRam = 0 + sysInfo.SysRamUsedPercent = 0 + } else { + sysInfo.SysTotalRam = uint32(sysRam.Total / 1024) + sysInfo.SysRamUsedPercent = uint16(sysRam.UsedPercent * 100) + } + + // partition usage + GetPartitions(sysInfo) + return nil +} + +func getProcess() process.Process { + checkPid := os.Getpid() + ret, _ := process.NewProcess(int32(checkPid)) + return *ret +} + +func GetSystemCpuInfo() { + physicalCnt, _ := cpu.Counts(false) + logicalCnt, _ := cpu.Counts(true) + log.Tracef("physical count:%d logical count:%d", physicalCnt, logicalCnt) + + totalPercent, _ := cpu.Percent(3*time.Second, false) // per cpu is false + perPercents, _ := cpu.Percent(3*time.Second, true) // per cpu is true + log.Tracef("total percent:%v per percents:%v", totalPercent, perPercents) +} + +func GetProcessCpuPercent() { + p := getProcess() + percent, err := p.Percent(0) + if err != nil { + log.Tracef("error %v", err) + } + + numcpu := runtime.NumCPU() + // if percent < 0.0 || percent > 100.0*float64(numcpu) { // TODO + if percent < 0.0 { + log.Tracef("Err CPU Percent of Process: %f, CPU num: %d", percent, numcpu) + } else { + log.Tracef("get process CPU percent: %f, CPU num: %d", percent, numcpu) + } +} + +func GetProcessMemoryInfo() { + p := getProcess() + + v, err := p.MemoryInfo() + if err != nil { + log.Tracef("getting memory info error %v", err) + } + + log.Tracef("get process memory info %v", v) + + info, _ := mem.VirtualMemory() + fmt.Println(info) +} + +func GetPartitions(sysInfo *SysInfo) { + sysInfo.PartitionNum = 0 + //sysInfo.PartitionInfo = make([]PartitionInfo, MAX_PARTITION_NUM, MAX_PARTITION_NUM) + infos, _ := disk.Partitions(true) + for _, info := range infos { + GetOnePartitionUsage(info.Mountpoint, sysInfo) + if sysInfo.PartitionNum >= MAX_PARTITION_NUM { + break + } + } +} + +func GetOnePartitionUsage(path string, sysInfo *SysInfo) int { + info, err := disk.Usage(path) + if err != nil { + return -1 + } + + if info.Total <= 0 { // info.Used/(1024 * 1024)MB + return 0 + } + var partition PartitionInfo + partition.Total = uint32(info.Total / 1024 / 1024) + partition.Used = uint32(info.Used / 1024 / 1024) + sysInfo.PartitionInfo = append(sysInfo.PartitionInfo, partition) + sysInfo.PartitionNum++ + + /*data, err := json.MarshalIndent(info, "", " ") + if err != nil { + return -1 + } + + fmt.Println(string(data))*/ + return 1 +} + +func getOS() string { + var osname string + if runtime.GOOS == "linux" { + osname = "GNU/Linux" + } + return osname +} + +func utsnameToString(unameArray [65]int8) string { + var byteString [65]byte + var indexLength int + for ; unameArray[indexLength] != 0 && indexLength < 65; indexLength++ { + byteString[indexLength] = uint8(unameArray[indexLength]) + } + return string(byteString[:indexLength]) +} + +func getUnameStr() string { + var utsname = syscall.Utsname{} + err := syscall.Uname(&utsname) + if err == nil { + name := utsnameToString(utsname.Sysname) + node := utsnameToString(utsname.Nodename) + release := utsnameToString(utsname.Release) + version := utsnameToString(utsname.Version) + machine := utsnameToString(utsname.Machine) + //domain:= utsnameToString(utsname.Domainname) + osName := getOS() + return fmt.Sprintf("%s %s %s %s %s %s", name, node, + release, version, machine, osName) + } + return "" +} diff --git a/features/state/state_windows.go b/features/state/state_windows.go new file mode 100644 index 0000000..548dbe3 --- /dev/null +++ b/features/state/state_windows.go @@ -0,0 +1,231 @@ +//go:build windows +// +build windows + +package state + +import ( + "encoding/binary" + "fmt" + "os" + "runtime" + "syscall" + "time" + + "github.com/shirou/gopsutil/cpu" + "github.com/shirou/gopsutil/disk" + "github.com/shirou/gopsutil/mem" + "github.com/shirou/gopsutil/process" +) + +type SysInfo struct { + SysCpuPercent uint16 // x% + MyCpuPercent uint16 // x%, CPU percent of current proccess + + SysTotalRam uint32 // KB + MyUsedRam uint32 // RAM usage of current proccess, KB + SysRamUsedPercent uint16 // x% + + PartitionNum byte + PartitionInfo []PartitionInfo // usage of each partition +} + +const MAX_PARTITION_NUM byte = 32 + +func GetSysStat() ([]byte, int) { + // Get sys info + var sysInfo SysInfo + err := GetSysInfo(&sysInfo) + if err != nil { + return nil, 0 + } + + //fmt.Printf("current sys info: %v", sysInfo) + + // build ems buffer + var data []byte = make([]byte, 1024) + var len int + var i byte + binary.BigEndian.PutUint16(data[0:], sysInfo.MyCpuPercent) //x% * 100 + binary.BigEndian.PutUint16(data[2:], sysInfo.SysCpuPercent) //x% * 100 + binary.BigEndian.PutUint32(data[4:], sysInfo.SysTotalRam) // KB + binary.BigEndian.PutUint32(data[8:], sysInfo.MyUsedRam) // KB + binary.BigEndian.PutUint16(data[12:], sysInfo.SysRamUsedPercent) //x% * 100 + data[14] = sysInfo.PartitionNum + for i = 0; i < sysInfo.PartitionNum; i++ { + binary.BigEndian.PutUint32(data[15+8*(i):], sysInfo.PartitionInfo[i].Total) // MB + binary.BigEndian.PutUint32(data[15+8*(i)+4:], sysInfo.PartitionInfo[i].Used) // MB + } + len = int(15 + 8*sysInfo.PartitionNum) + + //fmt.Printf("current sys stat buf: %v, len: %d", data, len) + + return data, len +} + +var pProc *process.Process = nil + +func GetSYsCpuPercent() float64 { + totalPercent, err := cpu.Percent(0, false) //(2*time.Second, false) + if err != nil { + return 0.0 + } else { + return totalPercent[0] + } +} + +func GetSysInfo(sysInfo *SysInfo) error { + // sys cpu percent + totalPercent, err := cpu.Percent(0, false) //(2*time.Second, false) + if err != nil { + sysInfo.SysCpuPercent = 0 + } else { + sysInfo.SysCpuPercent = uint16(totalPercent[0] * 100) + } + + if pProc == nil { + checkPid := os.Getpid() + pProc, err = process.NewProcess(int32(checkPid)) + if err != nil { + fmt.Printf("get process info error %v", err) + return err + } + } + + // self cpu percent + percent, err := pProc.Percent(0) //(2*time.Second) + if err != nil { + fmt.Printf("get process cpu percent error %v", err) + sysInfo.MyCpuPercent = 0 + } else { + sysInfo.MyCpuPercent = uint16(percent * 100) + } + + // self RAM(KB) + myRam, err := pProc.MemoryInfo() + if err != nil { + fmt.Printf("get self memory info error %v", err) + sysInfo.MyUsedRam = 0 + } else { + sysInfo.MyUsedRam = uint32(myRam.RSS / 1024) + } + + // system RAM(KB) + sysRam, err := mem.VirtualMemory() + if err != nil { + fmt.Printf("gett sys memory info error %v", err) + sysInfo.SysTotalRam = 0 + sysInfo.SysRamUsedPercent = 0 + } else { + sysInfo.SysTotalRam = uint32(sysRam.Total / 1024) + sysInfo.SysRamUsedPercent = uint16(sysRam.UsedPercent * 100) + } + + // partition usage + GetPartitions(sysInfo) + return nil +} + +func getProcess() process.Process { + checkPid := os.Getpid() + ret, _ := process.NewProcess(int32(checkPid)) + return *ret +} + +func GetSystemCpuInfo() { + physicalCnt, _ := cpu.Counts(false) + logicalCnt, _ := cpu.Counts(true) + fmt.Printf("physical count:%d logical count:%d\n", physicalCnt, logicalCnt) + + totalPercent, _ := cpu.Percent(3*time.Second, false) // per cpu is false + perPercents, _ := cpu.Percent(3*time.Second, true) // per cpu is true + fmt.Printf("total percent:%v per percents:%v\n", totalPercent, perPercents) +} + +func GetProcessCpuPercent() { + p := getProcess() + percent, err := p.Percent(0) + if err != nil { + fmt.Printf("error %v", err) + } + + numcpu := runtime.NumCPU() + // if percent < 0.0 || percent > 100.0*float64(numcpu) { // TODO + if percent < 0.0 { + fmt.Printf("Err CPU Percent of Process: %f, CPU num: %d", percent, numcpu) + } else { + fmt.Printf("get process CPU percent: %f, CPU num: %d", percent, numcpu) + } +} + +func GetProcessMemoryInfo() { + p := getProcess() + + v, err := p.MemoryInfo() + if err != nil { + fmt.Printf("getting memory info error %v", err) + } + + fmt.Printf("get process memory info %v\n", v) + + info, _ := mem.VirtualMemory() + fmt.Println(info) +} + +func GetPartitions(sysInfo *SysInfo) { + sysInfo.PartitionNum = 0 + //sysInfo.PartitionInfo = make([]PartitionInfo, MAX_PARTITION_NUM, MAX_PARTITION_NUM) + infos, _ := disk.Partitions(true) + for _, info := range infos { + GetOnePartitionUsage(info.Mountpoint, sysInfo) + if sysInfo.PartitionNum >= MAX_PARTITION_NUM { + break + } + } +} + +func GetOnePartitionUsage(path string, sysInfo *SysInfo) int { + info, err := disk.Usage(path) + if err != nil { + return -1 + } + + if info.Total <= 0 { // info.Used/(1024 * 1024)MB + return 0 + } + var partition PartitionInfo + partition.Total = uint32(info.Total / 1024 / 1024) + partition.Used = uint32(info.Used / 1024 / 1024) + sysInfo.PartitionInfo = append(sysInfo.PartitionInfo, partition) + sysInfo.PartitionNum++ + + /*data, err := json.MarshalIndent(info, "", " ") + if err != nil { + return -1 + } + + fmt.Println(string(data))*/ + return 1 +} + +func getOS() string { + var osname string + if runtime.GOOS == "linux" { + osname = "GNU/Linux" + } + return osname +} + +func utsnameToString(unameArray [65]int8) string { + var byteString [65]byte + var indexLength int + for ; unameArray[indexLength] != 0 && indexLength < 65; indexLength++ { + byteString[indexLength] = uint8(unameArray[indexLength]) + } + return string(byteString[:indexLength]) +} + +func getUnameStr() string { + osInfo, _ := syscall.GetVersion() + + return fmt.Sprintf("Widnows %d", osInfo) +} diff --git a/features/state/sysinfo.go b/features/state/sysinfo.go new file mode 100644 index 0000000..9aa9080 --- /dev/null +++ b/features/state/sysinfo.go @@ -0,0 +1,74 @@ +package state + +import ( + "ems.agt/lib/log" + "github.com/shirou/gopsutil/cpu" + "github.com/shirou/gopsutil/disk" + "github.com/shirou/gopsutil/host" + "github.com/shirou/gopsutil/mem" +) + +func getSystemInfo() { + // 获取主机信息 + hostInfo, err := host.Info() + if err != nil { + log.Errorf("Failed to get host info: %v", err) + return + } + log.Tracef("Host info: %+v", hostInfo) + + // 获取CPU信息 + cpuInfo, err := cpu.Info() + if err != nil { + log.Errorf("Failed to get CPU info: %v", err) + return + } + log.Tracef("CPU info: %+v", cpuInfo) + + // 获取内存信息 + memInfo, err := mem.VirtualMemory() + if err != nil { + log.Errorf("Failed to get memory info: %v", err) + return + } + log.Tracef("Memory info: %+v", memInfo) + + // 获取磁盘分区信息 + diskPartitions, err := disk.Partitions(true) + if err != nil { + log.Errorf("Failed to get disk partitions: %v", err) + return + } + log.Tracef("Disk partitions: %+v", diskPartitions) + for _, partition := range diskPartitions { + // 获取每个磁盘分区的使用情况 + usage, err := disk.Usage(partition.Mountpoint) + if err != nil { + log.Errorf("Failed to get disk usage for %s: %v", partition.Mountpoint, err) + continue + } + log.Tracef("%s usage: %+v", partition.Mountpoint, usage) + } +} + +func getCpuNumber() int { + // 获取CPU信息 + cpuInfo, err := cpu.Info() + if err != nil { + log.Errorf("Failed to get CPU info: %v", err) + return 0 + } + log.Tracef("CPU info: %+v", cpuInfo) + return len(cpuInfo) +} + +func getTotalMemory() int { + // 获取内存信息 + memInfo, err := mem.VirtualMemory() + if err != nil { + log.Errorf("Failed to get memory info: %v", err) + return 0 + } + log.Tracef("Memory info: %+v", memInfo) + return int(memInfo.Total) +} diff --git a/features/sys_config/api_sys_config.go b/features/sys_config/api_sys_config.go new file mode 100644 index 0000000..37019f9 --- /dev/null +++ b/features/sys_config/api_sys_config.go @@ -0,0 +1,230 @@ +package sysconfig + +import ( + "fmt" + "net/http" + "strings" + + "ems.agt/features/sys_config/model" + "ems.agt/features/sys_config/service" + "ems.agt/lib/core/utils/ctx" + "ems.agt/lib/core/utils/parse" + "ems.agt/lib/core/vo/result" + "ems.agt/lib/midware" + "ems.agt/lib/services" + "ems.agt/restagent/config" +) + +// 参数配置信息接口添加到路由 +func Routers() []services.RouterItem { + // 实例化控制层 SysConfigApi 结构体 + var apis = &SysConfigApi{ + sysConfigService: service.NewServiceSysConfig, + } + + rs := [...]services.RouterItem{ + { + Method: "GET", + Pattern: "/configs", + Handler: apis.List, + Middleware: midware.Authorize(nil), + }, + { + Method: "GET", + Pattern: "/config/{configId}", + Handler: apis.Info, + Middleware: midware.Authorize(nil), + }, + { + Method: "POST", + Pattern: "/config", + Handler: apis.Add, + Middleware: midware.Authorize(nil), + }, + { + Method: "PUT", + Pattern: "/config", + Handler: apis.Edit, + Middleware: midware.Authorize(nil), + }, + { + Method: "DELETE", + Pattern: "/config/{configIds}", + Handler: apis.Remove, + Middleware: midware.Authorize(nil), + }, + { + Method: "PUT", + Pattern: "/config/refreshCache", + Handler: apis.RefreshCache, + Middleware: midware.Authorize(nil), + }, + { + Method: "GET", + Pattern: "/config/configKey/{configKey}", + Handler: apis.ConfigKey, + Middleware: midware.Authorize(nil), + }, + // 添加更多的 Router 对象... + } + + // 生成两组前缀路由 + rsPrefix := []services.RouterItem{} + for _, v := range rs { + path := "/configManage/{apiVersion}" + v.Pattern + // 固定前缀 + v.Pattern = config.DefaultUriPrefix + path + rsPrefix = append(rsPrefix, v) + // 可配置 + v.Pattern = config.UriPrefix + path + rsPrefix = append(rsPrefix, v) + } + return rsPrefix +} + +// 参数配置信息 +// +// PATH /configManage +type SysConfigApi struct { + // 参数配置服务 + sysConfigService *service.ServiceSysConfig +} + +// 参数配置列表 +// +// GET /list +func (s *SysConfigApi) List(w http.ResponseWriter, r *http.Request) { + querys := ctx.QueryMap(r) + data := s.sysConfigService.SelectConfigPage(querys) + ctx.JSON(w, 200, result.Ok(data)) +} + +// 参数配置信息 +// +// GET /:configId +func (s *SysConfigApi) Info(w http.ResponseWriter, r *http.Request) { + configId := ctx.Param(r, "configId") + if configId == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + data := s.sysConfigService.SelectConfigById(configId) + if data.ConfigID == configId { + ctx.JSON(w, 200, result.OkData(data)) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} + +// 参数配置新增 +// +// POST / +func (s *SysConfigApi) Add(w http.ResponseWriter, r *http.Request) { + var body model.SysConfig + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.ConfigID != "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + // 检查属性值唯一 + uniqueConfigKey := s.sysConfigService.CheckUniqueConfigKey(body.ConfigKey, "") + if !uniqueConfigKey { + msg := fmt.Sprintf("参数配置新增【%s】失败,参数键名已存在", body.ConfigKey) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + body.CreateBy = ctx.LoginUserToUserName(r) + insertId := s.sysConfigService.InsertConfig(body) + if insertId != "" { + ctx.JSON(w, 200, result.Ok(nil)) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} + +// 参数配置修改 +// +// PUT / +func (s *SysConfigApi) Edit(w http.ResponseWriter, r *http.Request) { + var body model.SysConfig + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.ConfigID == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + // 检查属性值唯一 + uniqueConfigKey := s.sysConfigService.CheckUniqueConfigKey(body.ConfigKey, body.ConfigID) + if !uniqueConfigKey { + msg := fmt.Sprintf("参数配置修改【%s】失败,参数键名已存在", body.ConfigKey) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + // 检查是否存在 + config := s.sysConfigService.SelectConfigById(body.ConfigID) + if config.ConfigID != body.ConfigID { + ctx.JSON(w, 200, result.ErrMsg("没有权限访问参数配置数据!")) + return + } + + body.UpdateBy = ctx.LoginUserToUserName(r) + rows := s.sysConfigService.UpdateConfig(body) + if rows > 0 { + ctx.JSON(w, 200, result.Ok(nil)) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} + +// 参数配置删除 +// +// DELETE /:configIds +func (s *SysConfigApi) Remove(w http.ResponseWriter, r *http.Request) { + configIds := ctx.Param(r, "configIds") + if configIds == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + // 处理字符转id数组后去重 + ids := strings.Split(configIds, ",") + uniqueIDs := parse.RemoveDuplicates(ids) + if len(uniqueIDs) <= 0 { + ctx.JSON(w, 200, result.Err(nil)) + return + } + rows, err := s.sysConfigService.DeleteConfigByIds(uniqueIDs) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + msg := fmt.Sprintf("删除成功:%d", rows) + ctx.JSON(w, 200, result.OkMsg(msg)) +} + +// 参数配置刷新缓存 +// +// PUT /refreshCache +func (s *SysConfigApi) RefreshCache(w http.ResponseWriter, r *http.Request) { + s.sysConfigService.ResetConfigCache() + ctx.JSON(w, 200, result.Ok(nil)) +} + +// 参数配置根据参数键名 +// +// GET /configKey/:configKey +func (s *SysConfigApi) ConfigKey(w http.ResponseWriter, r *http.Request) { + configKey := ctx.Param(r, "configKey") + if configKey == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + key := s.sysConfigService.SelectConfigValueByKey(configKey) + if key != "" { + ctx.JSON(w, 200, result.OkData(key)) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} diff --git a/features/sys_config/model/sys_config.go b/features/sys_config/model/sys_config.go new file mode 100644 index 0000000..8949c67 --- /dev/null +++ b/features/sys_config/model/sys_config.go @@ -0,0 +1,25 @@ +package model + +// 参数配置对象 sys_config +type SysConfig struct { + // 参数主键 + ConfigID string `json:"configId"` + // 参数名称 + ConfigName string `json:"configName" binding:"required"` + // 参数键名 + ConfigKey string `json:"configKey" binding:"required"` + // 参数键值 + ConfigValue string `json:"configValue" binding:"required"` + // 系统内置(Y是 N否) + ConfigType string `json:"configType"` + // 创建者 + CreateBy string `json:"createBy"` + // 创建时间 + CreateTime int64 `json:"createTime"` + // 更新者 + UpdateBy string `json:"updateBy"` + // 更新时间 + UpdateTime int64 `json:"updateTime"` + // 备注 + Remark string `json:"remark"` +} diff --git a/features/sys_config/service/repo_sys_config.go b/features/sys_config/service/repo_sys_config.go new file mode 100644 index 0000000..488d7c9 --- /dev/null +++ b/features/sys_config/service/repo_sys_config.go @@ -0,0 +1,336 @@ +package service + +import ( + "fmt" + "strings" + "time" + + "ems.agt/features/sys_config/model" + "ems.agt/lib/core/datasource" + "ems.agt/lib/core/utils/date" + "ems.agt/lib/core/utils/parse" + "ems.agt/lib/log" +) + +// 实例化数据层 RepoSysConfig 结构体 +var NewRepoSysConfig = &RepoSysConfig{ + selectSql: `select + config_id, config_name, config_key, config_value, config_type, create_by, create_time, update_by, update_time, remark + from sys_config`, + + resultMap: map[string]string{ + "config_id": "ConfigID", + "config_name": "ConfigName", + "config_key": "ConfigKey", + "config_value": "ConfigValue", + "config_type": "ConfigType", + "remark": "Remark", + "create_by": "CreateBy", + "create_time": "CreateTime", + "update_by": "UpdateBy", + "update_time": "UpdateTime", + }, +} + +// RepoSysConfig 参数配置表 数据层处理 +type RepoSysConfig struct { + // 查询视图对象SQL + selectSql string + // 结果字段与实体映射 + resultMap map[string]string +} + +// convertResultRows 将结果记录转实体结果组 +func (r *RepoSysConfig) convertResultRows(rows []map[string]any) []model.SysConfig { + arr := make([]model.SysConfig, 0) + for _, row := range rows { + sysConfig := model.SysConfig{} + for key, value := range row { + if keyMapper, ok := r.resultMap[key]; ok { + datasource.SetFieldValue(&sysConfig, keyMapper, value) + } + } + arr = append(arr, sysConfig) + } + return arr +} + +// SelectDictDataPage 分页查询参数配置列表数据 +func (r *RepoSysConfig) SelectConfigPage(query map[string]any) map[string]any { + // 查询条件拼接 + var conditions []string + var params []any + if v, ok := query["configName"]; ok && v != "" { + conditions = append(conditions, "config_name like concat(?, '%')") + params = append(params, v) + } + if v, ok := query["configType"]; ok && v != "" { + conditions = append(conditions, "config_type = ?") + params = append(params, v) + } + if v, ok := query["configKey"]; ok && v != "" { + conditions = append(conditions, "config_key like concat(?, '%')") + params = append(params, v) + } + beginTime, ok := query["beginTime"] + if !ok { + beginTime, ok = query["params[beginTime]"] + } + if ok && beginTime != "" { + conditions = append(conditions, "create_time >= ?") + beginDate := date.ParseStrToDate(beginTime.(string), date.YYYY_MM_DD) + params = append(params, beginDate.UnixMilli()) + } + endTime, ok := query["endTime"] + if !ok { + endTime, ok = query["params[endTime]"] + } + if ok && endTime != "" { + conditions = append(conditions, "create_time <= ?") + endDate := date.ParseStrToDate(endTime.(string), date.YYYY_MM_DD) + params = append(params, endDate.UnixMilli()) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询数量 长度为0直接返回 + totalSql := "select count(1) as 'total' from sys_config" + totalRows, err := datasource.RawDB("", totalSql+whereSql, params) + if err != nil { + log.Errorf("total err => %v", err) + } + total := parse.Number(totalRows[0]["total"]) + if total == 0 { + return map[string]any{ + "total": total, + "rows": []model.SysConfig{}, + } + } + + // 分页 + pageNum, pageSize := datasource.PageNumSize(query["pageNum"], query["pageSize"]) + pageSql := " limit ?,? " + params = append(params, pageNum*pageSize) + params = append(params, pageSize) + + // 查询数据 + querySql := r.selectSql + whereSql + pageSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + log.Errorf("query err => %v", err) + } + + // 转换实体 + rows := r.convertResultRows(results) + return map[string]any{ + "total": total, + "rows": rows, + } +} + +// SelectConfigList 查询参数配置列表 +func (r *RepoSysConfig) SelectConfigList(sysConfig model.SysConfig) []model.SysConfig { + // 查询条件拼接 + var conditions []string + var params []any + if sysConfig.ConfigName != "" { + conditions = append(conditions, "config_name like concat(?, '%')") + params = append(params, sysConfig.ConfigName) + } + if sysConfig.ConfigType != "" { + conditions = append(conditions, "config_type = ?") + params = append(params, sysConfig.ConfigType) + } + if sysConfig.ConfigKey != "" { + conditions = append(conditions, "config_key like concat(?, '%')") + params = append(params, sysConfig.ConfigKey) + } + if sysConfig.CreateTime > 0 { + conditions = append(conditions, "create_time >= ?") + params = append(params, sysConfig.CreateTime) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询数据 + querySql := r.selectSql + whereSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + log.Errorf("query err => %v", err) + return []model.SysConfig{} + } + + // 转换实体 + return r.convertResultRows(results) +} + +// SelectConfigValueByKey 通过参数键名查询参数键值 +func (r *RepoSysConfig) SelectConfigValueByKey(configKey string) string { + querySql := "select config_value as 'str' from sys_config where config_key = ?" + results, err := datasource.RawDB("", querySql, []any{configKey}) + if err != nil { + log.Errorf("query err => %v", err) + return "" + } + if len(results) > 0 { + return fmt.Sprintf("%v", results[0]["str"]) + } + return "" +} + +// SelectConfigByIds 通过配置ID查询参数配置信息 +func (r *RepoSysConfig) SelectConfigByIds(configIds []string) []model.SysConfig { + placeholder := datasource.KeyPlaceholderByQuery(len(configIds)) + querySql := r.selectSql + " where config_id in (" + placeholder + ")" + parameters := datasource.ConvertIdsSlice(configIds) + results, err := datasource.RawDB("", querySql, parameters) + if err != nil { + log.Errorf("query err => %v", err) + return []model.SysConfig{} + } + // 转换实体 + return r.convertResultRows(results) +} + +// CheckUniqueConfig 校验配置参数是否唯一 +func (r *RepoSysConfig) CheckUniqueConfig(sysConfig model.SysConfig) string { + // 查询条件拼接 + var conditions []string + var params []any + if sysConfig.ConfigKey != "" { + conditions = append(conditions, "config_key = ?") + params = append(params, sysConfig.ConfigKey) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } else { + return "" + } + + // 查询数据 + querySql := "select config_id as 'str' from sys_config " + whereSql + " limit 1" + results, err := datasource.RawDB("", querySql, params) + if err != nil { + log.Errorf("query err %v", err) + return "" + } + if len(results) > 0 { + return fmt.Sprintf("%v", results[0]["str"]) + } + return "" +} + +// InsertConfig 新增参数配置 +func (r *RepoSysConfig) InsertConfig(sysConfig model.SysConfig) string { + // 参数拼接 + params := make(map[string]any) + if sysConfig.ConfigName != "" { + params["config_name"] = sysConfig.ConfigName + } + if sysConfig.ConfigKey != "" { + params["config_key"] = sysConfig.ConfigKey + } + if sysConfig.ConfigValue != "" { + params["config_value"] = sysConfig.ConfigValue + } + if sysConfig.ConfigType != "" { + params["config_type"] = sysConfig.ConfigType + } + if sysConfig.Remark != "" { + params["remark"] = sysConfig.Remark + } + if sysConfig.CreateBy != "" { + params["create_by"] = sysConfig.CreateBy + params["create_time"] = time.Now().UnixMilli() + } + + // 构建执行语句 + keys, placeholder, values := datasource.KeyPlaceholderValueByInsert(params) + sql := "insert into sys_config (" + strings.Join(keys, ",") + ")values(" + placeholder + ")" + + // 执行插入 + results, err := datasource.ExecDB("", sql, values) + if err != nil { + log.Errorf("insert row : %v", err.Error()) + return "" + } + + insertId, err := results.LastInsertId() + if err != nil { + log.Errorf("insert row : %v", err.Error()) + return "" + } + return fmt.Sprint(insertId) +} + +// UpdateConfig 修改参数配置 +func (r *RepoSysConfig) UpdateConfig(sysConfig model.SysConfig) int64 { + // 参数拼接 + params := make(map[string]any) + if sysConfig.ConfigName != "" { + params["config_name"] = sysConfig.ConfigName + } + if sysConfig.ConfigKey != "" { + params["config_key"] = sysConfig.ConfigKey + } + if sysConfig.ConfigValue != "" { + params["config_value"] = sysConfig.ConfigValue + } + if sysConfig.ConfigType != "" { + params["config_type"] = sysConfig.ConfigType + } + if sysConfig.Remark != "" { + params["remark"] = sysConfig.Remark + } + if sysConfig.UpdateBy != "" { + params["update_by"] = sysConfig.UpdateBy + params["update_time"] = time.Now().UnixMilli() + } + + // 构建执行语句 + keys, values := datasource.KeyValueByUpdate(params) + sql := "update sys_config set " + strings.Join(keys, ",") + " where config_id = ?" + + // 执行更新 + values = append(values, sysConfig.ConfigID) + results, err := datasource.ExecDB("", sql, values) + if err != nil { + log.Errorf("update row : %v", err.Error()) + return 0 + } + affected, err := results.RowsAffected() + if err != nil { + log.Errorf("update err => %v", err) + return 0 + } + return affected +} + +// DeleteConfigByIds 批量删除参数配置信息 +func (r *RepoSysConfig) DeleteConfigByIds(configIds []string) int64 { + placeholder := datasource.KeyPlaceholderByQuery(len(configIds)) + sql := "delete from sys_config where config_id in (" + placeholder + ")" + parameters := datasource.ConvertIdsSlice(configIds) + results, err := datasource.ExecDB("", sql, parameters) + if err != nil { + log.Errorf("delete err => %v", err) + return 0 + } + affected, err := results.RowsAffected() + if err != nil { + log.Errorf("delete err => %v", err) + return 0 + } + return affected +} diff --git a/features/sys_config/service/service_sys_config.go b/features/sys_config/service/service_sys_config.go new file mode 100644 index 0000000..ed0bf3c --- /dev/null +++ b/features/sys_config/service/service_sys_config.go @@ -0,0 +1,155 @@ +package service + +import ( + "errors" + + "ems.agt/features/sys_config/model" + "ems.agt/lib/core/cache" + "ems.agt/lib/core/constants/cachekey" +) + +// 实例化服务层 ServiceSysConfig 结构体 +var NewServiceSysConfig = &ServiceSysConfig{ + sysConfigRepository: NewRepoSysConfig, +} + +// ServiceSysConfig 参数配置 服务层处理 +type ServiceSysConfig struct { + // 参数配置表 + sysConfigRepository *RepoSysConfig +} + +// SelectDictDataPage 分页查询参数配置列表数据 +func (r *ServiceSysConfig) SelectConfigPage(query map[string]any) map[string]any { + return r.sysConfigRepository.SelectConfigPage(query) +} + +// SelectConfigList 查询参数配置列表 +func (r *ServiceSysConfig) SelectConfigList(sysConfig model.SysConfig) []model.SysConfig { + return r.sysConfigRepository.SelectConfigList(sysConfig) +} + +// SelectConfigValueByKey 通过参数键名查询参数键值 +func (r *ServiceSysConfig) SelectConfigValueByKey(configKey string) string { + cacheKey := r.getCacheKey(configKey) + // 从缓存中读取 + cacheValue, ok := cache.GetLocal(cacheKey) + if cacheValue != nil && ok { + return cacheValue.(string) + } + // 无缓存时读取数据放入缓存中 + configValue := r.sysConfigRepository.SelectConfigValueByKey(configKey) + if configValue != "" { + cache.SetLocal(cacheKey, configValue) + return configValue + } + return "" +} + +// SelectConfigById 通过配置ID查询参数配置信息 +func (r *ServiceSysConfig) SelectConfigById(configId string) model.SysConfig { + if configId == "" { + return model.SysConfig{} + } + configs := r.sysConfigRepository.SelectConfigByIds([]string{configId}) + if len(configs) > 0 { + return configs[0] + } + return model.SysConfig{} +} + +// CheckUniqueConfigKey 校验参数键名是否唯一 +func (r *ServiceSysConfig) CheckUniqueConfigKey(configKey, configId string) bool { + uniqueId := r.sysConfigRepository.CheckUniqueConfig(model.SysConfig{ + ConfigKey: configKey, + }) + if uniqueId == configId { + return true + } + return uniqueId == "" +} + +// InsertConfig 新增参数配置 +func (r *ServiceSysConfig) InsertConfig(sysConfig model.SysConfig) string { + configId := r.sysConfigRepository.InsertConfig(sysConfig) + if configId != "" { + r.loadingConfigCache(sysConfig.ConfigKey) + } + return configId +} + +// UpdateConfig 修改参数配置 +func (r *ServiceSysConfig) UpdateConfig(sysConfig model.SysConfig) int64 { + rows := r.sysConfigRepository.UpdateConfig(sysConfig) + if rows > 0 { + r.loadingConfigCache(sysConfig.ConfigKey) + } + return rows +} + +// DeleteConfigByIds 批量删除参数配置信息 +func (r *ServiceSysConfig) DeleteConfigByIds(configIds []string) (int64, error) { + // 检查是否存在 + configs := r.sysConfigRepository.SelectConfigByIds(configIds) + if len(configs) <= 0 { + return 0, errors.New("没有权限访问参数配置数据!") + } + for _, config := range configs { + // 检查是否为内置参数 + if config.ConfigType == "Y" { + return 0, errors.New(config.ConfigID + " 配置参数属于内置参数,禁止删除!") + } + // 清除缓存 + r.clearConfigCache(config.ConfigKey) + } + if len(configs) == len(configIds) { + rows := r.sysConfigRepository.DeleteConfigByIds(configIds) + return rows, nil + } + return 0, errors.New("删除参数配置信息失败!") +} + +// ResetConfigCache 重置参数缓存数据 +func (r *ServiceSysConfig) ResetConfigCache() { + r.clearConfigCache("*") + r.loadingConfigCache("*") +} + +// getCacheKey 组装缓存key +func (r *ServiceSysConfig) getCacheKey(configKey string) string { + return cachekey.SYS_CONFIG_KEY + configKey +} + +// loadingConfigCache 加载参数缓存数据 +func (r *ServiceSysConfig) loadingConfigCache(configKey string) { + // 查询全部参数 + if configKey == "*" { + sysConfigs := r.SelectConfigList(model.SysConfig{}) + for _, v := range sysConfigs { + key := r.getCacheKey(v.ConfigKey) + cache.DeleteLocal(key) + cache.SetLocal(key, v.ConfigValue) + } + return + } + // 指定参数 + if configKey != "" { + cacheValue := r.sysConfigRepository.SelectConfigValueByKey(configKey) + if cacheValue != "" { + key := r.getCacheKey(configKey) + cache.DeleteLocal(key) + cache.SetLocal(key, cacheValue) + } + return + } +} + +// clearConfigCache 清空参数缓存数据 +func (r *ServiceSysConfig) clearConfigCache(configKey string) bool { + key := r.getCacheKey(configKey) + keys := cache.GetLocalKeys(key) + for _, v := range keys { + cache.DeleteLocal(v) + } + return len(keys) > 0 +} diff --git a/features/sys_dict_data/api_sys_dict_data.go b/features/sys_dict_data/api_sys_dict_data.go new file mode 100644 index 0000000..05dbb08 --- /dev/null +++ b/features/sys_dict_data/api_sys_dict_data.go @@ -0,0 +1,247 @@ +package sysdictdata + +import ( + "fmt" + "net/http" + "strings" + + "ems.agt/features/sys_dict_data/model" + sysDictDataService "ems.agt/features/sys_dict_data/service" + sysDictTypeService "ems.agt/features/sys_dict_type/service" + "ems.agt/lib/core/utils/ctx" + "ems.agt/lib/core/utils/parse" + "ems.agt/lib/core/vo/result" + "ems.agt/lib/midware" + "ems.agt/lib/services" + "ems.agt/restagent/config" +) + +// 字典类型对应的字典数据信息接口添加到路由 +func Routers() []services.RouterItem { + // 实例化控制层 SysDictDataApi 结构体 + var apis = &SysDictDataApi{ + sysDictDataService: sysDictDataService.NewServiceSysDictData, + sysDictTypeService: sysDictTypeService.NewServiceSysDictType, + } + + rs := [...]services.RouterItem{ + { + Method: "GET", + Pattern: "/dictDatas", + Handler: apis.List, + Middleware: midware.Authorize(nil), + }, + { + Method: "GET", + Pattern: "/dictData/{dictCode}", + Handler: apis.Info, + Middleware: midware.Authorize(nil), + }, + { + Method: "POST", + Pattern: "/dictData", + Handler: apis.Add, + Middleware: midware.Authorize(nil), + }, + { + Method: "PUT", + Pattern: "/dictData", + Handler: apis.Edit, + Middleware: midware.Authorize(nil), + }, + { + Method: "DELETE", + Pattern: "/dictData/{dictCodes}", + Handler: apis.Remove, + Middleware: midware.Authorize(nil), + }, + { + Method: "GET", + Pattern: "/dictData/type/{dictType}", + Handler: apis.DictType, + Middleware: midware.Authorize(nil), + }, + // 添加更多的 Router 对象... + } + + // 生成两组前缀路由 + rsPrefix := []services.RouterItem{} + for _, v := range rs { + path := "/dictDataManage/{apiVersion}" + v.Pattern + // 固定前缀 + v.Pattern = config.DefaultUriPrefix + path + rsPrefix = append(rsPrefix, v) + // 可配置 + v.Pattern = config.UriPrefix + path + rsPrefix = append(rsPrefix, v) + } + return rsPrefix +} + +// 字典类型对应的字典数据信息 +// +// PATH /dictDataManage +type SysDictDataApi struct { + // 字典数据服务 + sysDictDataService *sysDictDataService.ServiceSysDictData + // 字典类型服务 + sysDictTypeService *sysDictTypeService.ServiceSysDictType +} + +// 字典数据列表 +// +// GET /list +func (s *SysDictDataApi) List(w http.ResponseWriter, r *http.Request) { + querys := ctx.QueryMap(r) + data := s.sysDictDataService.SelectDictDataPage(querys) + ctx.JSON(w, 200, result.Ok(data)) +} + +// 字典数据详情 +// +// GET /:dictCode +func (s *SysDictDataApi) Info(w http.ResponseWriter, r *http.Request) { + dictCode := ctx.Param(r, "dictCode") + if dictCode == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + data := s.sysDictDataService.SelectDictDataByCode(dictCode) + if data.DictCode == dictCode { + ctx.JSON(w, 200, result.OkData(data)) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} + +// 字典数据新增 +// +// POST / +func (s *SysDictDataApi) Add(w http.ResponseWriter, r *http.Request) { + var body model.SysDictData + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.DictCode != "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + // 检查字典类型是否存在 + sysDictType := s.sysDictTypeService.SelectDictTypeByType(body.DictType) + if sysDictType.DictType != body.DictType { + ctx.JSON(w, 200, result.ErrMsg("没有权限访问字典类型数据!")) + return + } + + // 检查字典标签唯一 + uniqueDictLabel := s.sysDictDataService.CheckUniqueDictLabel(body.DictType, body.DictLabel, "") + if !uniqueDictLabel { + msg := fmt.Sprintf("数据新增【%s】失败,该字典类型下标签名已存在", body.DictLabel) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + // 检查字典键值唯一 + uniqueDictValue := s.sysDictDataService.CheckUniqueDictValue(body.DictType, body.DictValue, "") + if !uniqueDictValue { + msg := fmt.Sprintf("数据新增【%s】失败,该字典类型下标签值已存在", body.DictValue) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + body.CreateBy = ctx.LoginUserToUserName(r) + insertId := s.sysDictDataService.InsertDictData(body) + if insertId != "" { + ctx.JSON(w, 200, result.Ok(nil)) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} + +// 字典类型修改 +// +// PUT / +func (s *SysDictDataApi) Edit(w http.ResponseWriter, r *http.Request) { + var body model.SysDictData + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.DictCode == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + // 检查字典类型是否存在 + sysDictType := s.sysDictTypeService.SelectDictTypeByType(body.DictType) + if sysDictType.DictType != body.DictType { + ctx.JSON(w, 200, result.ErrMsg("没有权限访问字典类型数据!")) + return + } + + // 检查字典编码是否存在 + SysDictDataApi := s.sysDictDataService.SelectDictDataByCode(body.DictCode) + if SysDictDataApi.DictCode != body.DictCode { + ctx.JSON(w, 200, result.ErrMsg("没有权限访问字典编码数据!")) + return + } + + // 检查字典标签唯一 + uniqueDictLabel := s.sysDictDataService.CheckUniqueDictLabel(body.DictType, body.DictLabel, body.DictCode) + if !uniqueDictLabel { + msg := fmt.Sprintf("数据修改【%s】失败,该字典类型下标签名已存在", body.DictLabel) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + // 检查字典键值唯一 + uniqueDictValue := s.sysDictDataService.CheckUniqueDictValue(body.DictType, body.DictValue, body.DictCode) + if !uniqueDictValue { + msg := fmt.Sprintf("数据修改【%s】失败,该字典类型下标签值已存在", body.DictValue) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + body.UpdateBy = ctx.LoginUserToUserName(r) + rows := s.sysDictDataService.UpdateDictData(body) + if rows > 0 { + ctx.JSON(w, 200, result.Ok(nil)) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} + +// 字典数据删除 +// +// DELETE /:dictCodes +func (s *SysDictDataApi) Remove(w http.ResponseWriter, r *http.Request) { + dictCodes := ctx.Param(r, "dictCodes") + if dictCodes == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + // 处理字符转id数组后去重 + ids := strings.Split(dictCodes, ",") + uniqueIDs := parse.RemoveDuplicates(ids) + if len(uniqueIDs) <= 0 { + ctx.JSON(w, 200, result.Err(nil)) + return + } + rows, err := s.sysDictDataService.DeleteDictDataByCodes(uniqueIDs) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + msg := fmt.Sprintf("删除成功:%d", rows) + ctx.JSON(w, 200, result.OkMsg(msg)) +} + +// 字典数据列表(指定字典类型) +// +// GET /type/:dictType +func (s *SysDictDataApi) DictType(w http.ResponseWriter, r *http.Request) { + dictType := ctx.Param(r, "dictType") + if dictType == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + data := s.sysDictDataService.SelectDictDataByType(dictType) + ctx.JSON(w, 200, result.OkData(data)) +} diff --git a/features/sys_dict_data/model/sys_dict_data.go b/features/sys_dict_data/model/sys_dict_data.go new file mode 100644 index 0000000..7c8cd06 --- /dev/null +++ b/features/sys_dict_data/model/sys_dict_data.go @@ -0,0 +1,31 @@ +package model + +// SysDictData 字典数据对象 sys_dict_data +type SysDictData struct { + // 字典编码 + DictCode string `json:"dictCode"` + // 字典排序 + DictSort int `json:"dictSort"` + // 字典标签 + DictLabel string `json:"dictLabel" binding:"required"` + // 字典键值 + DictValue string `json:"dictValue" binding:"required"` + // 字典类型 + DictType string `json:"dictType" binding:"required"` + // 样式属性(样式扩展) + TagClass string `json:"tagClass"` + // 标签类型(预设颜色) + TagType string `json:"tagType"` + // 状态(0停用 1正常) + Status string `json:"status"` + // 创建者 + CreateBy string `json:"createBy"` + // 创建时间 + CreateTime int64 `json:"createTime"` + // 更新者 + UpdateBy string `json:"updateBy"` + // 更新时间 + UpdateTime int64 `json:"updateTime"` + // 备注 + Remark string `json:"remark"` +} diff --git a/features/sys_dict_data/repo/repo_sys_dict_data.go b/features/sys_dict_data/repo/repo_sys_dict_data.go new file mode 100644 index 0000000..cd6268a --- /dev/null +++ b/features/sys_dict_data/repo/repo_sys_dict_data.go @@ -0,0 +1,369 @@ +package repo + +import ( + "fmt" + "strings" + "time" + + "ems.agt/features/sys_dict_data/model" + "ems.agt/lib/core/datasource" + "ems.agt/lib/core/utils/parse" + "ems.agt/lib/log" +) + +// 实例化数据层 RepoSysDictData 结构体 +var NewRepoSysDictData = &RepoSysDictData{ + selectSql: `select + dict_code, dict_sort, dict_label, dict_value, dict_type, tag_class, tag_type, status, create_by, create_time, remark + from sys_dict_data`, + + resultMap: map[string]string{ + "dict_code": "DictCode", + "dict_sort": "DictSort", + "dict_label": "DictLabel", + "dict_value": "DictValue", + "dict_type": "DictType", + "tag_class": "TagClass", + "tag_type": "TagType", + "status": "Status", + "remark": "Remark", + "create_by": "CreateBy", + "create_time": "CreateTime", + "update_by": "UpdateBy", + "update_time": "UpdateTime", + }, +} + +// RepoSysDictData 字典类型数据表 数据层处理 +type RepoSysDictData struct { + // 查询视图对象SQL + selectSql string + // 结果字段与实体映射 + resultMap map[string]string +} + +// convertResultRows 将结果记录转实体结果组 +func (r *RepoSysDictData) convertResultRows(rows []map[string]any) []model.SysDictData { + arr := make([]model.SysDictData, 0) + for _, row := range rows { + sysDictData := model.SysDictData{} + for key, value := range row { + if keyMapper, ok := r.resultMap[key]; ok { + datasource.SetFieldValue(&sysDictData, keyMapper, value) + } + } + arr = append(arr, sysDictData) + } + return arr +} + +// SelectDictDataPage 根据条件分页查询字典数据 +func (r *RepoSysDictData) SelectDictDataPage(query map[string]any) map[string]any { + // 查询条件拼接 + var conditions []string + var params []any + if v, ok := query["dictType"]; ok && v != "" { + conditions = append(conditions, "dict_type = ?") + params = append(params, v) + } + if v, ok := query["dictLabel"]; ok && v != "" { + conditions = append(conditions, "dict_label like concat(?, '%')") + params = append(params, v) + } + if v, ok := query["status"]; ok && v != "" { + conditions = append(conditions, "status = ?") + params = append(params, v) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询数量 长度为0直接返回 + totalSql := "select count(1) as 'total' from sys_dict_data" + totalRows, err := datasource.RawDB("", totalSql+whereSql, params) + if err != nil { + log.Errorf("total err => %v", err) + } + total := parse.Number(totalRows[0]["total"]) + if total == 0 { + return map[string]any{ + "total": total, + "rows": []model.SysDictData{}, + } + } + + // 分页 + pageNum, pageSize := datasource.PageNumSize(query["pageNum"], query["pageSize"]) + pageSql := " order by dict_sort asc limit ?,? " + params = append(params, pageNum*pageSize) + params = append(params, pageSize) + + // 查询数据 + querySql := r.selectSql + whereSql + pageSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + log.Errorf("query err => %v", err) + } + + // 转换实体 + rows := r.convertResultRows(results) + return map[string]any{ + "total": total, + "rows": rows, + } +} + +// SelectDictDataList 根据条件查询字典数据 +func (r *RepoSysDictData) SelectDictDataList(sysDictData model.SysDictData) []model.SysDictData { + // 查询条件拼接 + var conditions []string + var params []any + if sysDictData.DictLabel != "" { + conditions = append(conditions, "dict_label like concat(?, '%')") + params = append(params, sysDictData.DictLabel) + } + if sysDictData.DictType != "" { + conditions = append(conditions, "dict_type = ?") + params = append(params, sysDictData.DictType) + } + if sysDictData.Status != "" { + conditions = append(conditions, "status = ?") + params = append(params, sysDictData.Status) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询数据 + orderSql := " order by dict_sort asc " + querySql := r.selectSql + whereSql + orderSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + log.Errorf("query err => %v", err) + return []model.SysDictData{} + } + + // 转换实体 + return r.convertResultRows(results) +} + +// SelectDictDataByCodes 根据字典数据编码查询信息 +func (r *RepoSysDictData) SelectDictDataByCodes(dictCodes []string) []model.SysDictData { + placeholder := datasource.KeyPlaceholderByQuery(len(dictCodes)) + querySql := r.selectSql + " where dict_code in (" + placeholder + ")" + parameters := datasource.ConvertIdsSlice(dictCodes) + results, err := datasource.RawDB("", querySql, parameters) + if err != nil { + log.Errorf("query err => %v", err) + return []model.SysDictData{} + } + // 转换实体 + return r.convertResultRows(results) +} + +// CountDictDataByType 查询字典数据 +func (r *RepoSysDictData) CountDictDataByType(dictType string) int64 { + querySql := "select count(1) as 'total' from sys_dict_data where dict_type = ?" + results, err := datasource.RawDB("", querySql, []any{dictType}) + if err != nil { + log.Errorf("query err => %v", err) + return 0 + } + if len(results) > 0 { + return parse.Number(results[0]["total"]) + } + return 0 +} + +// CheckUniqueDictData 校验字典数据是否唯一 +func (r *RepoSysDictData) CheckUniqueDictData(sysDictData model.SysDictData) string { + // 查询条件拼接 + var conditions []string + var params []any + if sysDictData.DictType != "" { + conditions = append(conditions, "dict_type = ?") + params = append(params, sysDictData.DictType) + } + if sysDictData.DictLabel != "" { + conditions = append(conditions, "dict_label = ?") + params = append(params, sysDictData.DictLabel) + } + if sysDictData.DictValue != "" { + conditions = append(conditions, "dict_value = ?") + params = append(params, sysDictData.DictValue) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } else { + return "" + } + + // 查询数据 + querySql := "select dict_code as 'str' from sys_dict_data " + whereSql + " limit 1" + results, err := datasource.RawDB("", querySql, params) + if err != nil { + log.Errorf("query err %v", err) + return "" + } + if len(results) > 0 { + return fmt.Sprintf("%v", results[0]["str"]) + } + return "" +} + +// DeleteDictDataByCodes 批量删除字典数据信息 +func (r *RepoSysDictData) DeleteDictDataByCodes(dictCodes []string) int64 { + placeholder := datasource.KeyPlaceholderByQuery(len(dictCodes)) + sql := "delete from sys_dict_data where dict_code in (" + placeholder + ")" + parameters := datasource.ConvertIdsSlice(dictCodes) + results, err := datasource.ExecDB("", sql, parameters) + if err != nil { + log.Errorf("delete err => %v", err) + return 0 + } + affected, err := results.RowsAffected() + if err != nil { + log.Errorf("delete err => %v", err) + return 0 + } + return affected +} + +// InsertDictData 新增字典数据信息 +func (r *RepoSysDictData) InsertDictData(sysDictData model.SysDictData) string { + // 参数拼接 + params := make(map[string]any) + if sysDictData.DictSort > 0 { + params["dict_sort"] = sysDictData.DictSort + } + if sysDictData.DictLabel != "" { + params["dict_label"] = sysDictData.DictLabel + } + if sysDictData.DictValue != "" { + params["dict_value"] = sysDictData.DictValue + } + if sysDictData.DictType != "" { + params["dict_type"] = sysDictData.DictType + } + if sysDictData.TagClass != "" { + params["tag_class"] = sysDictData.TagClass + } + if sysDictData.TagType != "" { + params["tag_type"] = sysDictData.TagType + } + if sysDictData.Status != "" { + params["status"] = sysDictData.Status + } + if sysDictData.Remark != "" { + params["remark"] = sysDictData.Remark + } + if sysDictData.CreateBy != "" { + params["create_by"] = sysDictData.CreateBy + params["create_time"] = time.Now().UnixMilli() + } + + // 构建执行语句 + keys, placeholder, values := datasource.KeyPlaceholderValueByInsert(params) + sql := "insert into sys_dict_data (" + strings.Join(keys, ",") + ")values(" + placeholder + ")" + + // 执行插入 + results, err := datasource.ExecDB("", sql, values) + if err != nil { + log.Errorf("insert row : %v", err.Error()) + return "" + } + insertId, err := results.LastInsertId() + if err != nil { + log.Errorf("insert row : %v", err.Error()) + return "" + } + return fmt.Sprint(insertId) +} + +// UpdateDictData 修改字典数据信息 +func (r *RepoSysDictData) UpdateDictData(sysDictData model.SysDictData) int64 { + // 参数拼接 + params := make(map[string]any) + if sysDictData.DictSort > 0 { + params["dict_sort"] = sysDictData.DictSort + } + if sysDictData.DictLabel != "" { + params["dict_label"] = sysDictData.DictLabel + } + if sysDictData.DictValue != "" { + params["dict_value"] = sysDictData.DictValue + } + if sysDictData.DictType != "" { + params["dict_type"] = sysDictData.DictType + } + if sysDictData.TagClass != "" { + params["tag_class"] = sysDictData.TagClass + } + if sysDictData.TagType != "" { + params["tag_type"] = sysDictData.TagType + } + if sysDictData.Status != "" { + params["status"] = sysDictData.Status + } + if sysDictData.Remark != "" { + params["remark"] = sysDictData.Remark + } + if sysDictData.UpdateBy != "" { + params["update_by"] = sysDictData.UpdateBy + params["update_time"] = time.Now().UnixMilli() + } + + // 构建执行语句 + keys, values := datasource.KeyValueByUpdate(params) + sql := "update sys_dict_data set " + strings.Join(keys, ",") + " where dict_code = ?" + + // 执行更新 + values = append(values, sysDictData.DictCode) + results, err := datasource.ExecDB("", sql, values) + if err != nil { + log.Errorf("update row : %v", err.Error()) + return 0 + } + affected, err := results.RowsAffected() + if err != nil { + log.Errorf("update err => %v", err) + return 0 + } + return affected +} + +// UpdateDictDataType 同步修改字典类型 +func (r *RepoSysDictData) UpdateDictDataType(oldDictType string, newDictType string) int64 { + // 参数拼接 + params := make([]any, 0) + if oldDictType == "" || newDictType == "" { + return 0 + } + params = append(params, newDictType) + params = append(params, oldDictType) + + // 构建执行语句 + sql := "update sys_dict_data set dict_type = ? where dict_type = ?" + + // 执行更新 + results, err := datasource.ExecDB("", sql, params) + if err != nil { + log.Errorf("update row : %v", err.Error()) + return 0 + } + affected, err := results.RowsAffected() + if err != nil { + log.Errorf("update err => %v", err) + return 0 + } + return affected +} diff --git a/features/sys_dict_data/service/service_sys_dict_data.go b/features/sys_dict_data/service/service_sys_dict_data.go new file mode 100644 index 0000000..624105e --- /dev/null +++ b/features/sys_dict_data/service/service_sys_dict_data.go @@ -0,0 +1,111 @@ +package service + +import ( + "errors" + + "ems.agt/features/sys_dict_data/model" + "ems.agt/features/sys_dict_data/repo" + sysDictTypeService "ems.agt/features/sys_dict_type/service" +) + +// 实例化服务层 ServiceSysDictData 结构体 +var NewServiceSysDictData = &ServiceSysDictData{ + sysDictDataRepository: *repo.NewRepoSysDictData, + sysDictTypeService: *sysDictTypeService.NewServiceSysDictType, +} + +// ServiceSysDictData 字典类型数据 服务层处理 +type ServiceSysDictData struct { + // 字典数据服务 + sysDictDataRepository repo.RepoSysDictData + // 字典类型服务 + sysDictTypeService sysDictTypeService.ServiceSysDictType +} + +// SelectDictDataPage 根据条件分页查询字典数据 +func (r *ServiceSysDictData) SelectDictDataPage(query map[string]any) map[string]any { + return r.sysDictDataRepository.SelectDictDataPage(query) +} + +// SelectDictDataList 根据条件查询字典数据 +func (r *ServiceSysDictData) SelectDictDataList(sysDictData model.SysDictData) []model.SysDictData { + return r.sysDictDataRepository.SelectDictDataList(sysDictData) +} + +// SelectDictDataByCode 根据字典数据编码查询信息 +func (r *ServiceSysDictData) SelectDictDataByCode(dictCode string) model.SysDictData { + if dictCode == "" { + return model.SysDictData{} + } + dictCodes := r.sysDictDataRepository.SelectDictDataByCodes([]string{dictCode}) + if len(dictCodes) > 0 { + return dictCodes[0] + } + return model.SysDictData{} +} + +// SelectDictDataByType 根据字典类型查询信息 +func (r *ServiceSysDictData) SelectDictDataByType(dictType string) []model.SysDictData { + return r.sysDictTypeService.DictDataCache(dictType) +} + +// CheckUniqueDictLabel 校验字典标签是否唯一 +func (r *ServiceSysDictData) CheckUniqueDictLabel(dictType, dictLabel, dictCode string) bool { + uniqueId := r.sysDictDataRepository.CheckUniqueDictData(model.SysDictData{ + DictType: dictType, + DictLabel: dictLabel, + }) + if uniqueId == dictCode { + return true + } + return uniqueId == "" +} + +// CheckUniqueDictValue 校验字典键值是否唯一 +func (r *ServiceSysDictData) CheckUniqueDictValue(dictType, dictValue, dictCode string) bool { + uniqueId := r.sysDictDataRepository.CheckUniqueDictData(model.SysDictData{ + DictType: dictType, + DictValue: dictValue, + }) + if uniqueId == dictCode { + return true + } + return uniqueId == "" +} + +// DeleteDictDataByCodes 批量删除字典数据信息 +func (r *ServiceSysDictData) DeleteDictDataByCodes(dictCodes []string) (int64, error) { + // 检查是否存在 + dictDatas := r.sysDictDataRepository.SelectDictDataByCodes(dictCodes) + if len(dictDatas) <= 0 { + return 0, errors.New("没有权限访问字典编码数据!") + } + if len(dictDatas) == len(dictCodes) { + for _, v := range dictDatas { + // 刷新缓存 + r.sysDictTypeService.ClearDictCache(v.DictType) + r.sysDictTypeService.LoadingDictCache(v.DictType) + } + rows := r.sysDictDataRepository.DeleteDictDataByCodes(dictCodes) + return rows, nil + } + return 0, errors.New("删除字典数据信息失败!") +} + +// InsertDictData 新增字典数据信息 +func (r *ServiceSysDictData) InsertDictData(sysDictData model.SysDictData) string { + insertId := r.sysDictDataRepository.InsertDictData(sysDictData) + if insertId != "" { + r.sysDictTypeService.LoadingDictCache(sysDictData.DictType) + } + return insertId +} + +// UpdateDictData 修改字典数据信息 +func (r *ServiceSysDictData) UpdateDictData(sysDictData model.SysDictData) int64 { + rows := r.sysDictDataRepository.UpdateDictData(sysDictData) + if rows > 0 { + r.sysDictTypeService.LoadingDictCache(sysDictData.DictType) + } + return rows +} diff --git a/features/sys_dict_type/api_sys_dict_type.go b/features/sys_dict_type/api_sys_dict_type.go new file mode 100644 index 0000000..480ff01 --- /dev/null +++ b/features/sys_dict_type/api_sys_dict_type.go @@ -0,0 +1,253 @@ +package sysdicttype + +import ( + "fmt" + "net/http" + "strings" + + "ems.agt/features/sys_dict_type/model" + sysDictTypeService "ems.agt/features/sys_dict_type/service" + "ems.agt/lib/core/utils/ctx" + "ems.agt/lib/core/utils/parse" + "ems.agt/lib/core/vo/result" + "ems.agt/lib/midware" + "ems.agt/lib/services" + "ems.agt/restagent/config" +) + +// 字典类型信息接口添加到路由 +func Routers() []services.RouterItem { + // 实例化控制层 SysDictTypeApi 结构体 + var apis = &SysDictTypeApi{ + sysDictTypeService: *sysDictTypeService.NewServiceSysDictType, + } + + rs := [...]services.RouterItem{ + { + Method: "GET", + Pattern: "/dictTypes", + Handler: apis.List, + Middleware: midware.Authorize(nil), + }, + { + Method: "GET", + Pattern: "/dictType/{dictId}", + Handler: apis.Info, + Middleware: midware.Authorize(nil), + }, + { + Method: "POST", + Pattern: "/dictType", + Handler: apis.Add, + Middleware: midware.Authorize(nil), + }, + { + Method: "PUT", + Pattern: "/dictType", + Handler: apis.Edit, + Middleware: midware.Authorize(nil), + }, + { + Method: "DELETE", + Pattern: "/dictType/{dictIds}", + Handler: apis.Remove, + Middleware: midware.Authorize(nil), + }, + { + Method: "PUT", + Pattern: "/dictType/refreshCache", + Handler: apis.RefreshCache, + Middleware: midware.Authorize(nil), + }, + { + Method: "GET", + Pattern: "/dictTypes/optionselect", + Handler: apis.DictOptionselect, + Middleware: midware.Authorize(nil), + }, + // 添加更多的 Router 对象... + } + + // 生成两组前缀路由 + rsPrefix := []services.RouterItem{} + for _, v := range rs { + path := "/dictTypegManage/{apiVersion}" + v.Pattern + // 固定前缀 + v.Pattern = config.DefaultUriPrefix + path + rsPrefix = append(rsPrefix, v) + // 可配置 + v.Pattern = config.UriPrefix + path + rsPrefix = append(rsPrefix, v) + } + return rsPrefix +} + +// 字典类型信息 +// +// PATH /dictTypegManage +type SysDictTypeApi struct { + // 字典类型服务 + sysDictTypeService sysDictTypeService.ServiceSysDictType +} + +// 字典类型列表 +// +// GET /list +func (s *SysDictTypeApi) List(w http.ResponseWriter, r *http.Request) { + querys := ctx.QueryMap(r) + data := s.sysDictTypeService.SelectDictTypePage(querys) + ctx.JSON(w, 200, result.Ok(data)) +} + +// 字典类型信息 +// +// GET /:dictId +func (s *SysDictTypeApi) Info(w http.ResponseWriter, r *http.Request) { + dictId := ctx.Param(r, "dictId") + if dictId == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + data := s.sysDictTypeService.SelectDictTypeByID(dictId) + if data.DictID == dictId { + ctx.JSON(w, 200, result.OkData(data)) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} + +// 字典类型新增 +// +// POST / +func (s *SysDictTypeApi) Add(w http.ResponseWriter, r *http.Request) { + var body model.SysDictType + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.DictID != "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + // 检查字典名称唯一 + uniqueDictName := s.sysDictTypeService.CheckUniqueDictName(body.DictName, "") + if !uniqueDictName { + msg := fmt.Sprintf("字典新增【%s】失败,字典名称已存在", body.DictName) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + // 检查字典类型唯一 + uniqueDictType := s.sysDictTypeService.CheckUniqueDictType(body.DictType, "") + if !uniqueDictType { + msg := fmt.Sprintf("字典新增【%s】失败,字典类型已存在", body.DictType) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + body.CreateBy = ctx.LoginUserToUserName(r) + insertId := s.sysDictTypeService.InsertDictType(body) + if insertId != "" { + ctx.JSON(w, 200, result.Ok(nil)) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} + +// 字典类型修改 +// +// PUT / +func (s *SysDictTypeApi) Edit(w http.ResponseWriter, r *http.Request) { + var body model.SysDictType + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.DictID == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + // 检查数据是否存在 + dictInfo := s.sysDictTypeService.SelectDictTypeByID(body.DictID) + if dictInfo.DictID != body.DictID { + ctx.JSON(w, 200, result.ErrMsg("没有权限访问字典类型数据!")) + return + } + + // 检查字典名称唯一 + uniqueDictName := s.sysDictTypeService.CheckUniqueDictName(body.DictName, body.DictID) + if !uniqueDictName { + msg := fmt.Sprintf("字典修改【%s】失败,字典名称已存在", body.DictName) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + // 检查字典类型唯一 + uniqueDictType := s.sysDictTypeService.CheckUniqueDictType(body.DictType, body.DictID) + if !uniqueDictType { + msg := fmt.Sprintf("字典修改【%s】失败,字典类型已存在", body.DictType) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + body.UpdateBy = ctx.LoginUserToUserName(r) + rows := s.sysDictTypeService.UpdateDictType(body) + if rows > 0 { + ctx.JSON(w, 200, result.Ok(nil)) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} + +// 字典类型删除 +// +// DELETE /:dictIds +func (s *SysDictTypeApi) Remove(w http.ResponseWriter, r *http.Request) { + dictIds := ctx.Param(r, "dictIds") + if dictIds == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + // 处理字符转id数组后去重 + ids := strings.Split(dictIds, ",") + uniqueIDs := parse.RemoveDuplicates(ids) + if len(uniqueIDs) <= 0 { + ctx.JSON(w, 200, result.Err(nil)) + return + } + rows, err := s.sysDictTypeService.DeleteDictTypeByIDs(uniqueIDs) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + msg := fmt.Sprintf("删除成功:%d", rows) + ctx.JSON(w, 200, result.OkMsg(msg)) +} + +// 字典类型刷新缓存 +// +// PUT /refreshCache +func (s *SysDictTypeApi) RefreshCache(w http.ResponseWriter, r *http.Request) { + s.sysDictTypeService.ResetDictCache() + ctx.JSON(w, 200, result.Ok(nil)) +} + +// 字典类型选择框列表 +// +// GET /getDictOptionselect +func (s *SysDictTypeApi) DictOptionselect(w http.ResponseWriter, r *http.Request) { + data := s.sysDictTypeService.SelectDictTypeList(model.SysDictType{ + Status: "1", + }) + + type labelValue struct { + Label string `json:"label"` + Value string `json:"value"` + } + + // 数据组 + arr := []labelValue{} + for _, v := range data { + arr = append(arr, labelValue{ + Label: v.DictName, + Value: v.DictType, + }) + } + ctx.JSON(w, 200, result.OkData(arr)) +} diff --git a/features/sys_dict_type/model/sys_dict_type.go b/features/sys_dict_type/model/sys_dict_type.go new file mode 100644 index 0000000..0ad0b1a --- /dev/null +++ b/features/sys_dict_type/model/sys_dict_type.go @@ -0,0 +1,23 @@ +package model + +// SysDictType 字典类型对象 sys_dict_type +type SysDictType struct { + // 字典主键 + DictID string `json:"dictId"` + // 字典名称 + DictName string `json:"dictName" binding:"required"` + // 字典类型 + DictType string `json:"dictType" binding:"required"` + // 状态(0停用 1正常) + Status string `json:"status"` + // 创建者 + CreateBy string `json:"createBy"` + // 创建时间 + CreateTime int64 `json:"createTime"` + // 更新者 + UpdateBy string `json:"updateBy"` + // 更新时间 + UpdateTime int64 `json:"updateTime"` + // 备注 + Remark string `json:"remark"` +} diff --git a/features/sys_dict_type/repo/repo_sys_dict_type.go b/features/sys_dict_type/repo/repo_sys_dict_type.go new file mode 100644 index 0000000..874dac1 --- /dev/null +++ b/features/sys_dict_type/repo/repo_sys_dict_type.go @@ -0,0 +1,330 @@ +package repo + +import ( + "fmt" + "strings" + "time" + + "ems.agt/features/sys_dict_type/model" + "ems.agt/lib/core/datasource" + "ems.agt/lib/core/utils/date" + "ems.agt/lib/core/utils/parse" + "ems.agt/lib/log" +) + +// 实例化数据层 RepoSysDictType 结构体 +var NewRepoSysDictType = &RepoSysDictType{ + selectSql: `select + dict_id, dict_name, dict_type, status, create_by, create_time, remark + from sys_dict_type`, + + resultMap: map[string]string{ + "dict_id": "DictID", + "dict_name": "DictName", + "dict_type": "DictType", + "remark": "Remark", + "status": "Status", + "create_by": "CreateBy", + "create_time": "CreateTime", + "update_by": "UpdateBy", + "update_time": "UpdateTime", + }, +} + +// RepoSysDictType 字典类型表 数据层处理 +type RepoSysDictType struct { + // 查询视图对象SQL + selectSql string + // 结果字段与实体映射 + resultMap map[string]string +} + +// convertResultRows 将结果记录转实体结果组 +func (r *RepoSysDictType) convertResultRows(rows []map[string]any) []model.SysDictType { + arr := make([]model.SysDictType, 0) + for _, row := range rows { + sysDictType := model.SysDictType{} + for key, value := range row { + if keyMapper, ok := r.resultMap[key]; ok { + datasource.SetFieldValue(&sysDictType, keyMapper, value) + } + } + arr = append(arr, sysDictType) + } + return arr +} + +// SelectDictTypePage 根据条件分页查询字典类型 +func (r *RepoSysDictType) SelectDictTypePage(query map[string]any) map[string]any { + // 查询条件拼接 + var conditions []string + var params []any + if v, ok := query["dictName"]; ok && v != "" { + conditions = append(conditions, "dict_name like concat(?, '%')") + params = append(params, v) + } + if v, ok := query["dictType"]; ok && v != "" { + conditions = append(conditions, "dict_type like concat(?, '%')") + params = append(params, v) + } + if v, ok := query["status"]; ok && v != "" { + conditions = append(conditions, "status = ?") + params = append(params, v) + } + beginTime, ok := query["beginTime"] + if !ok { + beginTime, ok = query["params[beginTime]"] + } + if ok && beginTime != "" { + conditions = append(conditions, "create_time >= ?") + beginDate := date.ParseStrToDate(beginTime.(string), date.YYYY_MM_DD) + params = append(params, beginDate.UnixMilli()) + } + endTime, ok := query["endTime"] + if !ok { + endTime, ok = query["params[endTime]"] + } + if ok && endTime != "" { + conditions = append(conditions, "create_time <= ?") + endDate := date.ParseStrToDate(endTime.(string), date.YYYY_MM_DD) + params = append(params, endDate.UnixMilli()) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询数量 长度为0直接返回 + totalSql := "select count(1) as 'total' from sys_dict_type" + totalRows, err := datasource.RawDB("", totalSql+whereSql, params) + if err != nil { + log.Errorf("total err => %v", err) + } + total := parse.Number(totalRows[0]["total"]) + if total == 0 { + return map[string]any{ + "total": total, + "rows": []model.SysDictType{}, + } + } + + // 分页 + pageNum, pageSize := datasource.PageNumSize(query["pageNum"], query["pageSize"]) + pageSql := " limit ?,? " + params = append(params, pageNum*pageSize) + params = append(params, pageSize) + + // 查询数据 + querySql := r.selectSql + whereSql + pageSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + log.Errorf("query err => %v", err) + } + + // 转换实体 + rows := r.convertResultRows(results) + return map[string]any{ + "total": total, + "rows": rows, + } +} + +// SelectDictTypeList 根据条件查询字典类型 +func (r *RepoSysDictType) SelectDictTypeList(sysDictType model.SysDictType) []model.SysDictType { + // 查询条件拼接 + var conditions []string + var params []any + if sysDictType.DictName != "" { + conditions = append(conditions, "dict_name like concat(?, '%')") + params = append(params, sysDictType.DictName) + } + if sysDictType.DictType != "" { + conditions = append(conditions, "dict_type like concat(?, '%')") + params = append(params, sysDictType.DictType) + } + if sysDictType.Status != "" { + conditions = append(conditions, "status = ?") + params = append(params, sysDictType.Status) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询数据 + querySql := r.selectSql + whereSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + log.Errorf("query err => %v", err) + return []model.SysDictType{} + } + + // 转换实体 + return r.convertResultRows(results) +} + +// SelectDictTypeByIDs 根据字典类型ID查询信息 +func (r *RepoSysDictType) SelectDictTypeByIDs(dictIDs []string) []model.SysDictType { + placeholder := datasource.KeyPlaceholderByQuery(len(dictIDs)) + querySql := r.selectSql + " where dict_id in (" + placeholder + ")" + parameters := datasource.ConvertIdsSlice(dictIDs) + results, err := datasource.RawDB("", querySql, parameters) + if err != nil { + log.Errorf("query err => %v", err) + return []model.SysDictType{} + } + // 转换实体 + return r.convertResultRows(results) +} + +// SelectDictTypeByType 根据字典类型查询信息 +func (r *RepoSysDictType) SelectDictTypeByType(dictType string) model.SysDictType { + querySql := r.selectSql + " where dict_type = ?" + results, err := datasource.RawDB("", querySql, []any{dictType}) + if err != nil { + log.Errorf("query err => %v", err) + return model.SysDictType{} + } + // 转换实体 + rows := r.convertResultRows(results) + if len(rows) > 0 { + return rows[0] + } + return model.SysDictType{} +} + +// CheckUniqueDictType 校验字典是否唯一 +func (r *RepoSysDictType) CheckUniqueDictType(sysDictType model.SysDictType) string { + // 查询条件拼接 + var conditions []string + var params []any + if sysDictType.DictName != "" { + conditions = append(conditions, "dict_name = ?") + params = append(params, sysDictType.DictName) + } + if sysDictType.DictType != "" { + conditions = append(conditions, "dict_type = ?") + params = append(params, sysDictType.DictType) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } else { + return "" + } + + // 查询数据 + querySql := "select dict_id as 'str' from sys_dict_type " + whereSql + " limit 1" + results, err := datasource.RawDB("", querySql, params) + if err != nil { + log.Errorf("query err %v", err) + return "" + } + if len(results) > 0 { + return fmt.Sprintf("%v", results[0]["str"]) + } + return "" +} + +// InsertDictType 新增字典类型信息 +func (r *RepoSysDictType) InsertDictType(sysDictType model.SysDictType) string { + // 参数拼接 + params := make(map[string]any) + if sysDictType.DictName != "" { + params["dict_name"] = sysDictType.DictName + } + if sysDictType.DictType != "" { + params["dict_type"] = sysDictType.DictType + } + if sysDictType.Status != "" { + params["status"] = sysDictType.Status + } + if sysDictType.Remark != "" { + params["remark"] = sysDictType.Remark + } + if sysDictType.CreateBy != "" { + params["create_by"] = sysDictType.CreateBy + params["create_time"] = time.Now().UnixMilli() + } + + // 构建执行语句 + keys, placeholder, values := datasource.KeyPlaceholderValueByInsert(params) + sql := "insert into sys_dict_type (" + strings.Join(keys, ",") + ")values(" + placeholder + ")" + + // 执行插入 + results, err := datasource.ExecDB("", sql, values) + if err != nil { + log.Errorf("insert row : %v", err.Error()) + return "" + } + insertId, err := results.LastInsertId() + if err != nil { + log.Errorf("insert row : %v", err.Error()) + return "" + } + return fmt.Sprint(insertId) +} + +// UpdateDictType 修改字典类型信息 +func (r *RepoSysDictType) UpdateDictType(sysDictType model.SysDictType) int64 { + // 参数拼接 + params := make(map[string]any) + if sysDictType.DictName != "" { + params["dict_name"] = sysDictType.DictName + } + if sysDictType.DictType != "" { + params["dict_type"] = sysDictType.DictType + } + if sysDictType.Status != "" { + params["status"] = sysDictType.Status + } + if sysDictType.Remark != "" { + params["remark"] = sysDictType.Remark + } + if sysDictType.UpdateBy != "" { + params["update_by"] = sysDictType.UpdateBy + params["update_time"] = time.Now().UnixMilli() + } + + // 构建执行语句 + keys, values := datasource.KeyValueByUpdate(params) + sql := "update sys_dict_type set " + strings.Join(keys, ",") + " where dict_id = ?" + + // 执行更新 + values = append(values, sysDictType.DictID) + results, err := datasource.ExecDB("", sql, values) + if err != nil { + log.Errorf("update row : %v", err.Error()) + return 0 + } + affected, err := results.RowsAffected() + if err != nil { + log.Errorf("update err => %v", err) + return 0 + } + return affected +} + +// DeleteDictTypeByIDs 批量删除字典类型信息 +func (r *RepoSysDictType) DeleteDictTypeByIDs(dictIDs []string) int64 { + placeholder := datasource.KeyPlaceholderByQuery(len(dictIDs)) + sql := "delete from sys_dict_type where dict_id in (" + placeholder + ")" + parameters := datasource.ConvertIdsSlice(dictIDs) + results, err := datasource.ExecDB("", sql, parameters) + if err != nil { + log.Errorf("delete err => %v", err) + return 0 + } + affected, err := results.RowsAffected() + if err != nil { + log.Errorf("delete err => %v", err) + return 0 + } + return affected +} diff --git a/features/sys_dict_type/service/service_sys_dict_type.go b/features/sys_dict_type/service/service_sys_dict_type.go new file mode 100644 index 0000000..8cbb16b --- /dev/null +++ b/features/sys_dict_type/service/service_sys_dict_type.go @@ -0,0 +1,211 @@ +package service + +import ( + "encoding/json" + "errors" + "fmt" + + sysDictDataModel "ems.agt/features/sys_dict_data/model" + sysDictDataRepo "ems.agt/features/sys_dict_data/repo" + sysDictTypeModel "ems.agt/features/sys_dict_type/model" + "ems.agt/features/sys_dict_type/repo" + "ems.agt/lib/core/cache" + "ems.agt/lib/core/constants/cachekey" +) + +// 实例化服务层 ServiceSysDictType 结构体 +var NewServiceSysDictType = &ServiceSysDictType{ + sysDictTypeRepository: *repo.NewRepoSysDictType, + sysDictDataRepository: *sysDictDataRepo.NewRepoSysDictData, +} + +// ServiceSysDictType 字典类型 服务层处理 +type ServiceSysDictType struct { + // 字典类型服务 + sysDictTypeRepository repo.RepoSysDictType + // 字典数据服务 + sysDictDataRepository sysDictDataRepo.RepoSysDictData +} + +// SelectDictTypePage 根据条件分页查询字典类型 +func (r *ServiceSysDictType) SelectDictTypePage(query map[string]any) map[string]any { + return r.sysDictTypeRepository.SelectDictTypePage(query) +} + +// SelectDictTypeList 根据条件查询字典类型 +func (r *ServiceSysDictType) SelectDictTypeList(sysDictType sysDictTypeModel.SysDictType) []sysDictTypeModel.SysDictType { + return r.sysDictTypeRepository.SelectDictTypeList(sysDictType) +} + +// SelectDictTypeByID 根据字典类型ID查询信息 +func (r *ServiceSysDictType) SelectDictTypeByID(dictID string) sysDictTypeModel.SysDictType { + if dictID == "" { + return sysDictTypeModel.SysDictType{} + } + dictTypes := r.sysDictTypeRepository.SelectDictTypeByIDs([]string{dictID}) + if len(dictTypes) > 0 { + return dictTypes[0] + } + return sysDictTypeModel.SysDictType{} +} + +// SelectDictTypeByType 根据字典类型查询信息 +func (r *ServiceSysDictType) SelectDictTypeByType(dictType string) sysDictTypeModel.SysDictType { + return r.sysDictTypeRepository.SelectDictTypeByType(dictType) +} + +// CheckUniqueDictName 校验字典名称是否唯一 +func (r *ServiceSysDictType) CheckUniqueDictName(dictName, dictID string) bool { + uniqueId := r.sysDictTypeRepository.CheckUniqueDictType(sysDictTypeModel.SysDictType{ + DictName: dictName, + }) + if uniqueId == dictID { + return true + } + return uniqueId == "" +} + +// CheckUniqueDictType 校验字典类型是否唯一 +func (r *ServiceSysDictType) CheckUniqueDictType(dictType, dictID string) bool { + uniqueId := r.sysDictTypeRepository.CheckUniqueDictType(sysDictTypeModel.SysDictType{ + DictType: dictType, + }) + if uniqueId == dictID { + return true + } + return uniqueId == "" +} + +// InsertDictType 新增字典类型信息 +func (r *ServiceSysDictType) InsertDictType(sysDictType sysDictTypeModel.SysDictType) string { + insertId := r.sysDictTypeRepository.InsertDictType(sysDictType) + if insertId != "" { + r.LoadingDictCache(sysDictType.DictType) + } + return insertId +} + +// UpdateDictType 修改字典类型信息 +func (r *ServiceSysDictType) UpdateDictType(sysDictType sysDictTypeModel.SysDictType) int64 { + data := r.sysDictTypeRepository.SelectDictTypeByIDs([]string{sysDictType.DictID}) + if len(data) == 0 { + return 0 + } + // 修改字典类型key时同步更新其字典数据的类型key + oldDictType := data[0].DictType + rows := r.sysDictTypeRepository.UpdateDictType(sysDictType) + if rows > 0 && oldDictType != "" && oldDictType != sysDictType.DictType { + r.sysDictDataRepository.UpdateDictDataType(oldDictType, sysDictType.DictType) + } + // 刷新缓存 + r.ClearDictCache(oldDictType) + r.LoadingDictCache(sysDictType.DictType) + return rows +} + +// DeleteDictTypeByIDs 批量删除字典类型信息 +func (r *ServiceSysDictType) DeleteDictTypeByIDs(dictIDs []string) (int64, error) { + // 检查是否存在 + dictTypes := r.sysDictTypeRepository.SelectDictTypeByIDs(dictIDs) + if len(dictTypes) <= 0 { + return 0, errors.New("没有权限访问字典类型数据!") + } + for _, v := range dictTypes { + // 字典类型下级含有数据 + useCount := r.sysDictDataRepository.CountDictDataByType(v.DictType) + if useCount > 0 { + msg := fmt.Sprintf("【%s】存在字典数据,不能删除", v.DictName) + return 0, errors.New(msg) + } + // 清除缓存 + r.ClearDictCache(v.DictType) + } + if len(dictTypes) == len(dictIDs) { + rows := r.sysDictTypeRepository.DeleteDictTypeByIDs(dictIDs) + return rows, nil + } + return 0, errors.New("删除字典数据信息失败!") +} + +// ResetDictCache 重置字典缓存数据 +func (r *ServiceSysDictType) ResetDictCache() { + r.ClearDictCache("*") + r.LoadingDictCache("") +} + +// getCacheKey 组装缓存key +func (r *ServiceSysDictType) getDictCache(dictType string) string { + return cachekey.SYS_DICT_KEY + dictType +} + +// LoadingDictCache 加载字典缓存数据 +func (r *ServiceSysDictType) LoadingDictCache(dictType string) { + sysDictData := sysDictDataModel.SysDictData{ + Status: "1", + } + + // 指定字典类型 + if dictType != "" { + sysDictData.DictType = dictType + // 删除缓存 + key := r.getDictCache(dictType) + cache.DeleteLocal(key) + } + + sysDictDataList := r.sysDictDataRepository.SelectDictDataList(sysDictData) + if len(sysDictDataList) == 0 { + return + } + + // 将字典数据按类型分组 + m := make(map[string][]sysDictDataModel.SysDictData, 0) + for _, v := range sysDictDataList { + key := v.DictType + if item, ok := m[key]; ok { + m[key] = append(item, v) + } else { + m[key] = []sysDictDataModel.SysDictData{v} + } + } + + // 放入缓存 + for k, v := range m { + key := r.getDictCache(k) + values, _ := json.Marshal(v) + cache.SetLocal(key, string(values)) + } +} + +// ClearDictCache 清空字典缓存数据 +func (r *ServiceSysDictType) ClearDictCache(dictType string) bool { + key := r.getDictCache(dictType) + keys := cache.GetLocalKeys(key) + for _, v := range keys { + cache.DeleteLocal(v) + } + return len(keys) > 0 +} + +// DictDataCache 获取字典数据缓存数据 +func (r *ServiceSysDictType) DictDataCache(dictType string) []sysDictDataModel.SysDictData { + data := []sysDictDataModel.SysDictData{} + key := r.getDictCache(dictType) + jsonAny, ok := cache.GetLocal(key) + if jsonAny != nil && ok { + err := json.Unmarshal([]byte(jsonAny.(string)), &data) + if err != nil { + data = []sysDictDataModel.SysDictData{} + } + } else { + data = r.sysDictDataRepository.SelectDictDataList(sysDictDataModel.SysDictData{ + Status: "1", + DictType: dictType, + }) + if len(data) > 0 { + cache.DeleteLocal(key) + values, _ := json.Marshal(data) + cache.SetLocal(key, string(values)) + } + } + return data +} diff --git a/features/sys_menu/api_sys_menu.go b/features/sys_menu/api_sys_menu.go new file mode 100644 index 0000000..a78e0d8 --- /dev/null +++ b/features/sys_menu/api_sys_menu.go @@ -0,0 +1,354 @@ +package sysmenu + +import ( + "fmt" + "net/http" + + "ems.agt/features/sys_menu/consts" + "ems.agt/features/sys_menu/model" + "ems.agt/features/sys_menu/service" + "ems.agt/lib/core/conf" + "ems.agt/lib/core/utils/ctx" + "ems.agt/lib/core/utils/regular" + "ems.agt/lib/core/vo/result" + "ems.agt/lib/midware" + "ems.agt/lib/services" + "ems.agt/restagent/config" +) + +// 菜单接口添加到路由 +func Routers() []services.RouterItem { + // 实例化控制层 SysMenuApi 结构体 + var apis = &SysMenuApi{ + sysMenuService: service.NewServiceSysMenu, + } + + rs := [...]services.RouterItem{ + { + Method: "GET", + Pattern: "/menus", + Handler: apis.List, + Middleware: midware.Authorize(nil), + }, + { + Method: "GET", + Pattern: "/menu/{menuId}", + Handler: apis.Info, + Middleware: midware.Authorize(nil), + }, + { + Method: "POST", + Pattern: "/menu", + Handler: apis.Add, + Middleware: midware.Authorize(nil), + }, + { + Method: "PUT", + Pattern: "/menu", + Handler: apis.Edit, + Middleware: midware.Authorize(nil), + }, + { + Method: "DELETE", + Pattern: "/menu/{menuId}", + Handler: apis.Remove, + Middleware: midware.Authorize(nil), + }, + { + Method: "GET", + Pattern: "/menus/treeSelect", + Handler: apis.TreeSelect, + Middleware: midware.Authorize(nil), + }, + { + Method: "GET", + Pattern: "/menu/roleMenuTreeSelect/{roleId}", + Handler: apis.RoleMenuTreeSelect, + Middleware: midware.Authorize(nil), + }, + // 添加更多的 Router 对象... + } + + // 生成两组前缀路由 + rsPrefix := []services.RouterItem{} + for _, v := range rs { + path := "/menuManage/{apiVersion}" + v.Pattern + // 固定前缀 + v.Pattern = config.DefaultUriPrefix + path + rsPrefix = append(rsPrefix, v) + // 可配置 + v.Pattern = config.UriPrefix + path + rsPrefix = append(rsPrefix, v) + } + return rsPrefix +} + +// // 实例化控制层 SysMenuApi 结构体 +// var NewSysMenu = &SysMenuApi{ +// sysMenuService: NewServiceSysMenu, +// } + +// 菜单信息 +// +// PATH /menuManage +type SysMenuApi struct { + // 菜单服务 + sysMenuService *service.ServiceSysMenu +} + +// 菜单列表 +// +// GET /list +func (s *SysMenuApi) List(w http.ResponseWriter, r *http.Request) { + query := model.SysMenu{} + if v := ctx.GetQuery(r, "menuName"); v != "" { + query.MenuName = v + } + if v := ctx.GetQuery(r, "status"); v != "" { + query.Status = v + } + + userId := ctx.LoginUserToUserID(r) + if conf.IsAdmin(userId) { + userId = "*" + } + data := s.sysMenuService.SelectMenuList(query, userId) + ctx.JSON(w, 200, result.OkData(data)) +} + +// 菜单信息 +// +// GET /:menuId +func (s *SysMenuApi) Info(w http.ResponseWriter, r *http.Request) { + menuId := ctx.Param(r, "menuId") + if menuId == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + data := s.sysMenuService.SelectMenuById(menuId) + if data.MenuID == menuId { + ctx.JSON(w, 200, result.OkData(data)) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} + +// 菜单新增 +// +// POST / +func (s *SysMenuApi) Add(w http.ResponseWriter, r *http.Request) { + var body model.SysMenu + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.MenuID != "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + // 目录和菜单检查地址唯一 + if consts.TYPE_DIR == body.MenuType || consts.TYPE_MENU == body.MenuType { + uniqueNenuPath := s.sysMenuService.CheckUniqueMenuPath(body.Path, "") + if !uniqueNenuPath { + msg := fmt.Sprintf("菜单新增【%s】失败,菜单路由地址已存在", body.MenuName) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + } + + // 检查名称唯一 + uniqueNenuName := s.sysMenuService.CheckUniqueMenuName(body.MenuName, body.ParentID, "") + if !uniqueNenuName { + msg := fmt.Sprintf("菜单新增【%s】失败,菜单名称已存在", body.MenuName) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + // 外链菜单需要符合网站http(s)开头 + if body.IsFrame == "0" && !regular.ValidHttp(body.Path) { + msg := fmt.Sprintf("菜单新增【%s】失败,非内部地址必须以http(s)://开头", body.MenuName) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + body.CreateBy = ctx.LoginUserToUserName(r) + insertId := s.sysMenuService.InsertMenu(body) + if insertId != "" { + ctx.JSON(w, 200, result.Ok(nil)) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} + +// 菜单修改 +// +// PUT / +func (s *SysMenuApi) Edit(w http.ResponseWriter, r *http.Request) { + var body model.SysMenu + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.MenuID == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + // 上级菜单不能选自己 + if body.MenuID == body.ParentID { + msg := fmt.Sprintf("菜单修改【%s】失败,上级菜单不能选择自己", body.MenuName) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + // 检查数据是否存在 + menuInfo := s.sysMenuService.SelectMenuById(body.MenuID) + if menuInfo.MenuID != body.MenuID { + ctx.JSON(w, 200, result.ErrMsg("没有权限访问菜单数据")) + return + } + // 父级ID不为0是要检查 + if body.ParentID != "0" { + menuParent := s.sysMenuService.SelectMenuById(body.ParentID) + if menuParent.MenuID != body.ParentID { + ctx.JSON(w, 200, result.ErrMsg("没有权限访问菜单数据")) + return + } + // 禁用菜单时检查父菜单是否使用 + if body.Status == "1" && menuParent.Status == "0" { + ctx.JSON(w, 200, result.ErrMsg("父菜单未启用!")) + return + } + } + + // 目录和菜单检查地址唯一 + if consts.TYPE_DIR == body.MenuType || consts.TYPE_MENU == body.MenuType { + uniqueNenuPath := s.sysMenuService.CheckUniqueMenuPath(body.Path, body.MenuID) + if !uniqueNenuPath { + msg := fmt.Sprintf("菜单修改【%s】失败,菜单路由地址已存在", body.MenuName) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + } + + // 检查名称唯一 + uniqueNenuName := s.sysMenuService.CheckUniqueMenuName(body.MenuName, body.ParentID, body.MenuID) + if !uniqueNenuName { + msg := fmt.Sprintf("菜单修改【%s】失败,菜单名称已存在", body.MenuName) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + // 外链菜单需要符合网站http(s)开头 + if body.IsFrame == "0" && !regular.ValidHttp(body.Path) { + msg := fmt.Sprintf("菜单修改【%s】失败,非内部地址必须以http(s)://开头", body.MenuName) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + // 禁用菜单时检查子菜单是否使用 + if body.Status == "0" { + hasStatus := s.sysMenuService.HasChildByMenuIdAndStatus(body.MenuID, "1") + if hasStatus > 0 { + msg := fmt.Sprintf("不允许禁用,存在使用子菜单数:%d", hasStatus) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + } + + body.UpdateBy = ctx.LoginUserToUserName(r) + rows := s.sysMenuService.UpdateMenu(body) + if rows > 0 { + ctx.JSON(w, 200, result.Ok(nil)) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} + +// 菜单删除 +// +// DELETE /:menuId +func (s *SysMenuApi) Remove(w http.ResponseWriter, r *http.Request) { + menuId := ctx.Param(r, "menuId") + if menuId == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + // 检查数据是否存在 + menu := s.sysMenuService.SelectMenuById(menuId) + if menu.MenuID != menuId { + ctx.JSON(w, 200, result.ErrMsg("没有权限访问菜单数据!")) + return + } + + // 检查是否存在子菜单 + hasChild := s.sysMenuService.HasChildByMenuIdAndStatus(menuId, "") + if hasChild > 0 { + msg := fmt.Sprintf("不允许删除,存在子菜单数:%d", hasChild) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + // 检查是否分配给角色 + existRole := s.sysMenuService.CheckMenuExistRole(menuId) + if existRole > 0 { + msg := fmt.Sprintf("不允许删除,菜单已分配给角色数:%d", existRole) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + rows := s.sysMenuService.DeleteMenuById(menuId) + if rows > 0 { + msg := fmt.Sprintf("删除成功:%d", rows) + ctx.JSON(w, 200, result.OkMsg(msg)) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} + +// 菜单树结构列表 +// +// GET /treeSelect +func (s *SysMenuApi) TreeSelect(w http.ResponseWriter, r *http.Request) { + query := model.SysMenu{} + if v := ctx.GetQuery(r, "menuName"); v != "" { + query.MenuName = v + } + if v := ctx.GetQuery(r, "status"); v != "" { + query.Status = v + } + + userId := ctx.LoginUserToUserID(r) + if conf.IsAdmin(userId) { + userId = "*" + } + data := s.sysMenuService.SelectMenuTreeSelectByUserId(query, userId) + ctx.JSON(w, 200, result.OkData(data)) + +} + +// 菜单树结构列表(指定角色) +// +// GET /roleMenuTreeSelect/:roleId +func (s *SysMenuApi) RoleMenuTreeSelect(w http.ResponseWriter, r *http.Request) { + roleId := ctx.Param(r, "roleId") + if roleId == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + query := model.SysMenu{} + if v := ctx.GetQuery(r, "menuName"); v != "" { + query.MenuName = v + } + if v := ctx.GetQuery(r, "status"); v != "" { + query.Status = v + } + + userId := ctx.LoginUserToUserID(r) + if conf.IsAdmin(userId) { + userId = "*" + } + menuTreeSelect := s.sysMenuService.SelectMenuTreeSelectByUserId(query, userId) + checkedKeys := s.sysMenuService.SelectMenuListByRoleId(roleId) + ctx.JSON(w, 200, result.OkData(map[string]any{ + "menus": menuTreeSelect, + "checkedKeys": checkedKeys, + })) +} diff --git a/features/sys_menu/consts/consts_menu.go b/features/sys_menu/consts/consts_menu.go new file mode 100644 index 0000000..6a1dca3 --- /dev/null +++ b/features/sys_menu/consts/consts_menu.go @@ -0,0 +1,24 @@ +package consts + +// 系统菜单常量信息 + +const ( + // 组件布局类型-基础布局组件标识 + COMPONENT_LAYOUT_BASIC = "BasicLayout" + // 组件布局类型-空白布局组件标识 + COMPONENT_LAYOUT_BLANK = "BlankLayout" + // 组件布局类型-内链接布局组件标识 + COMPONENT_LAYOUT_LINK = "LinkLayout" +) + +const ( + // 菜单类型-目录 + TYPE_DIR = "D" + // 菜单类型-菜单 + TYPE_MENU = "M" + // 菜单类型-按钮 + TYPE_BUTTON = "B" +) + +// 菜单内嵌地址标识-带/前缀 +const PATH_INLINE = "/inline" diff --git a/features/sys_menu/model/sys_menu.go b/features/sys_menu/model/sys_menu.go new file mode 100644 index 0000000..397e30e --- /dev/null +++ b/features/sys_menu/model/sys_menu.go @@ -0,0 +1,46 @@ +package model + +// SysMenu 菜单权限对象 sys_menu +type SysMenu struct { + // 菜单ID + MenuID string `json:"menuId"` + // 菜单名称 + MenuName string `json:"menuName" binding:"required"` + // 父菜单ID 默认0 + ParentID string `json:"parentId" binding:"required"` + // 显示顺序 + MenuSort int `json:"menuSort"` + // 路由地址 + Path string `json:"path"` + // 组件路径 + Component string `json:"component"` + // 是否内部跳转(0否 1是) + IsFrame string `json:"isFrame"` + // 是否缓存(0不缓存 1缓存) + IsCache string `json:"isCache"` + // 菜单类型(D目录 M菜单 B按钮) + MenuType string `json:"menuType" binding:"required"` + // 是否显示(0隐藏 1显示) + Visible string `json:"visible"` + // 菜单状态(0停用 1正常) + Status string `json:"status"` + // 权限标识 + Perms string `json:"perms"` + // 菜单图标(#无图标) + Icon string `json:"icon"` + // 创建者 + CreateBy string `json:"createBy"` + // 创建时间 + CreateTime int64 `json:"createTime"` + // 更新者 + UpdateBy string `json:"updateBy"` + // 更新时间 + UpdateTime int64 `json:"updateTime"` + // 备注 + Remark string `json:"remark"` + + // ====== 非数据库字段属性 ====== + + // 子菜单 + Children []SysMenu `json:"children,omitempty"` +} diff --git a/features/sys_menu/service/repo_sys_menu.go b/features/sys_menu/service/repo_sys_menu.go new file mode 100644 index 0000000..af809a1 --- /dev/null +++ b/features/sys_menu/service/repo_sys_menu.go @@ -0,0 +1,475 @@ +package service + +import ( + "fmt" + "strings" + "time" + + "ems.agt/features/sys_menu/consts" + "ems.agt/features/sys_menu/model" + "ems.agt/lib/core/datasource" + "ems.agt/lib/core/utils/parse" + "ems.agt/lib/log" +) + +// 实例化数据层 RepoSysMenu 结构体 +var NewRepoSysMenu = &RepoSysMenu{ + selectSql: `select + m.menu_id, m.menu_name, m.parent_id, m.menu_sort, m.path, m.component, m.is_frame, m.is_cache, m.menu_type, m.visible, m.status, ifnull(m.perms,'') as perms, m.icon, m.create_time, m.remark + from sys_menu m`, + + selectSqlByUser: `select distinct + m.menu_id, m.menu_name, m.parent_id, m.menu_sort, m.path, m.component, m.is_frame, m.is_cache, m.menu_type, m.visible, m.status, ifnull(m.perms,'') as perms, m.icon, m.create_time, m.remark + from sys_menu m + left join sys_role_menu rm on m.menu_id = rm.menu_id + left join sys_user_role ur on rm.role_id = ur.role_id + left join sys_role ro on ur.role_id = ro.role_id`, + + resultMap: map[string]string{ + "menu_id": "MenuID", + "menu_name": "MenuName", + "parent_name": "ParentName", + "parent_id": "ParentID", + "path": "Path", + "menu_sort": "MenuSort", + "component": "Component", + "is_frame": "IsFrame", + "is_cache": "IsCache", + "menu_type": "MenuType", + "visible": "Visible", + "status": "Status", + "perms": "Perms", + "icon": "Icon", + "create_by": "CreateBy", + "create_time": "CreateTime", + "update_by": "UpdateBy", + "update_time": "UpdateTime", + "remark": "Remark", + }, +} + +// RepoSysMenu 菜单表 数据层处理 +type RepoSysMenu struct { + // 查询视图对象SQL + selectSql string + // 查询视图用户对象SQL + selectSqlByUser string + // 结果字段与实体映射 + resultMap map[string]string +} + +// convertResultRows 将结果记录转实体结果组 +func (r *RepoSysMenu) convertResultRows(rows []map[string]any) []model.SysMenu { + arr := make([]model.SysMenu, 0) + for _, row := range rows { + sysMenu := model.SysMenu{} + for key, value := range row { + if keyMapper, ok := r.resultMap[key]; ok { + datasource.SetFieldValue(&sysMenu, keyMapper, value) + } + } + arr = append(arr, sysMenu) + } + return arr +} + +// SelectMenuList 查询系统菜单列表 +func (r *RepoSysMenu) SelectMenuList(sysMenu model.SysMenu, userId string) []model.SysMenu { + // 查询条件拼接 + var conditions []string + var params []any + if sysMenu.MenuName != "" { + conditions = append(conditions, "m.menu_name like concat('%', concat(?, '%'))") + params = append(params, sysMenu.MenuName) + } + if sysMenu.Visible != "" { + conditions = append(conditions, "m.visible = ?") + params = append(params, sysMenu.Visible) + } + if sysMenu.Status != "" { + conditions = append(conditions, "m.status = ?") + params = append(params, sysMenu.Status) + } + + fromSql := r.selectSql + + // 个人菜单 + if userId != "*" { + fromSql = r.selectSqlByUser + conditions = append(conditions, "ur.user_id = ?") + params = append(params, userId) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询数据 + orderSql := " order by m.parent_id, m.menu_sort" + querySql := fromSql + whereSql + orderSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + log.Errorf("query err => %v", err) + return []model.SysMenu{} + } + + // 转换实体 + return r.convertResultRows(results) +} + +// SelectMenuPermsByUserId 根据用户ID查询权限 +func (r *RepoSysMenu) SelectMenuPermsByUserId(userId string) []string { + querySql := `select distinct m.perms as 'str' from sys_menu m + left join sys_role_menu rm on m.menu_id = rm.menu_id + left join sys_user_role ur on rm.role_id = ur.role_id + left join sys_role r on r.role_id = ur.role_id + where m.status = '1' and m.perms != '' and r.status = '1' and ur.user_id = ? ` + + // 查询结果 + results, err := datasource.RawDB("", querySql, []any{userId}) + if err != nil { + log.Errorf("query err => %v", err) + return []string{} + } + + // 读取结果 + rows := make([]string, 0) + for _, m := range results { + rows = append(rows, fmt.Sprintf("%v", m["str"])) + } + return rows +} + +// SelectMenuTreeByUserId 根据用户ID查询菜单 +func (r *RepoSysMenu) SelectMenuTreeByUserId(userId string) []model.SysMenu { + var params []any + var querySql string + + if userId == "*" { + // 管理员全部菜单 + querySql = r.selectSql + ` where + m.menu_type in (?,?) and m.status = '1' + order by m.parent_id, m.menu_sort` + params = append(params, consts.TYPE_DIR) + params = append(params, consts.TYPE_MENU) + } else { + // 用户ID权限 + querySql = r.selectSqlByUser + ` where + m.menu_type in (?, ?) and m.status = '1' + and ur.user_id = ? and ro.status = '1' + order by m.parent_id, m.menu_sort` + params = append(params, consts.TYPE_DIR) + params = append(params, consts.TYPE_MENU) + params = append(params, userId) + } + + // 查询结果 + results, err := datasource.RawDB("", querySql, params) + if err != nil { + log.Errorf("query err => %v", err) + return []model.SysMenu{} + } + + return r.convertResultRows(results) +} + +// SelectMenuListByRoleId 根据角色ID查询菜单树信息 +func (r *RepoSysMenu) SelectMenuListByRoleId(roleId string, menuCheckStrictly bool) []string { + querySql := `select m.menu_id as 'str' from sys_menu m + left join sys_role_menu rm on m.menu_id = rm.menu_id + where rm.role_id = ? ` + var params []any + params = append(params, roleId) + // 展开 + if menuCheckStrictly { + querySql += ` and m.menu_id not in + (select m.parent_id from sys_menu m + inner join sys_role_menu rm on m.menu_id = rm.menu_id + and rm.role_id = ?) ` + params = append(params, roleId) + } + + // 查询结果 + results, err := datasource.RawDB("", querySql, params) + if err != nil { + log.Errorf("query err => %v", err) + return []string{} + } + + if len(results) > 0 { + ids := make([]string, 0) + for _, v := range results { + ids = append(ids, fmt.Sprintf("%v", v["str"])) + } + return ids + } + return []string{} +} + +// SelectMenuByIds 根据菜单ID查询信息 +func (r *RepoSysMenu) SelectMenuByIds(menuIds []string) []model.SysMenu { + placeholder := datasource.KeyPlaceholderByQuery(len(menuIds)) + querySql := r.selectSql + " where m.menu_id in (" + placeholder + ")" + parameters := datasource.ConvertIdsSlice(menuIds) + results, err := datasource.RawDB("", querySql, parameters) + if err != nil { + log.Errorf("query err => %v", err) + return []model.SysMenu{} + } + // 转换实体 + return r.convertResultRows(results) +} + +// HasChildByMenuIdAndStatus 存在菜单子节点数量与状态 +func (r *RepoSysMenu) HasChildByMenuIdAndStatus(menuId, status string) int64 { + querySql := "select count(1) as 'total' from sys_menu where parent_id = ?" + params := []any{menuId} + + // 菜单状态 + if status != "" { + querySql += " and status = ?" + params = append(params, status) + } + + results, err := datasource.RawDB("", querySql, params) + if err != nil { + log.Errorf("query err => %v", err) + return 0 + } + if len(results) > 0 { + return parse.Number(results[0]["total"]) + } + return 0 +} + +// InsertMenu 新增菜单信息 +func (r *RepoSysMenu) InsertMenu(sysMenu model.SysMenu) string { + // 参数拼接 + params := make(map[string]any) + if sysMenu.MenuID != "" { + params["menu_id"] = sysMenu.MenuID + } + if sysMenu.ParentID != "" { + params["parent_id"] = sysMenu.ParentID + } + if sysMenu.MenuName != "" { + params["menu_name"] = sysMenu.MenuName + } + if sysMenu.MenuSort > 0 { + params["menu_sort"] = sysMenu.MenuSort + } + if sysMenu.Path != "" { + params["path"] = sysMenu.Path + } + if sysMenu.Component != "" { + params["component"] = sysMenu.Component + } + if sysMenu.IsFrame != "" { + params["is_frame"] = sysMenu.IsFrame + } + if sysMenu.IsCache != "" { + params["is_cache"] = sysMenu.IsCache + } + if sysMenu.MenuType != "" { + params["menu_type"] = sysMenu.MenuType + } + if sysMenu.Visible != "" { + params["visible"] = sysMenu.Visible + } + if sysMenu.Status != "" { + params["status"] = sysMenu.Status + } + if sysMenu.Perms != "" { + params["perms"] = sysMenu.Perms + } + if sysMenu.Icon != "" { + params["icon"] = sysMenu.Icon + } else { + params["icon"] = "#" + } + if sysMenu.Remark != "" { + params["remark"] = sysMenu.Remark + } + if sysMenu.CreateBy != "" { + params["create_by"] = sysMenu.CreateBy + params["create_time"] = time.Now().UnixMilli() + } + + // 根据菜单类型重置参数 + if sysMenu.MenuType == consts.TYPE_BUTTON { + params["component"] = "" + params["path"] = "" + params["icon"] = "#" + params["is_cache"] = "1" + params["is_frame"] = "1" + params["visible"] = "1" + params["status"] = "1" + } + if sysMenu.MenuType == consts.TYPE_DIR { + params["component"] = "" + params["perms"] = "" + } + + // 构建执行语句 + keys, placeholder, values := datasource.KeyPlaceholderValueByInsert(params) + sql := "insert into sys_menu (" + strings.Join(keys, ",") + ")values(" + placeholder + ")" + + // 执行插入 + results, err := datasource.ExecDB("", sql, values) + if err != nil { + log.Errorf("insert row : %v", err.Error()) + return "" + } + + insertId, err := results.LastInsertId() + if err != nil { + log.Errorf("insert row : %v", err.Error()) + return "" + } + return fmt.Sprint(insertId) +} + +// UpdateMenu 修改菜单信息 +func (r *RepoSysMenu) UpdateMenu(sysMenu model.SysMenu) int64 { + // 参数拼接 + params := make(map[string]any) + if sysMenu.MenuID != "" { + params["menu_id"] = sysMenu.MenuID + } + if sysMenu.ParentID != "" { + params["parent_id"] = sysMenu.ParentID + } + if sysMenu.MenuName != "" { + params["menu_name"] = sysMenu.MenuName + } + if sysMenu.MenuSort > 0 { + params["menu_sort"] = sysMenu.MenuSort + } + if sysMenu.Path != "" { + params["path"] = sysMenu.Path + } + if sysMenu.Component != "" { + params["component"] = sysMenu.Component + } + if sysMenu.IsFrame != "" { + params["is_frame"] = sysMenu.IsFrame + } + if sysMenu.IsCache != "" { + params["is_cache"] = sysMenu.IsCache + } + if sysMenu.MenuType != "" { + params["menu_type"] = sysMenu.MenuType + } + if sysMenu.Visible != "" { + params["visible"] = sysMenu.Visible + } + if sysMenu.Status != "" { + params["status"] = sysMenu.Status + } + if sysMenu.Perms != "" { + params["perms"] = sysMenu.Perms + } + if sysMenu.Icon != "" { + params["icon"] = sysMenu.Icon + } else { + params["icon"] = "#" + } + if sysMenu.Remark != "" { + params["remark"] = sysMenu.Remark + } + if sysMenu.UpdateBy != "" { + params["update_by"] = sysMenu.UpdateBy + params["update_time"] = time.Now().UnixMilli() + } + + // 根据菜单类型重置参数 + if sysMenu.MenuType == consts.TYPE_BUTTON { + params["component"] = "" + params["path"] = "" + params["icon"] = "#" + params["is_cache"] = "1" + params["is_frame"] = "1" + params["visible"] = "1" + params["status"] = "1" + } + if sysMenu.MenuType == consts.TYPE_DIR { + params["component"] = "" + params["perms"] = "" + } + + // 构建执行语句 + keys, values := datasource.KeyValueByUpdate(params) + sql := "update sys_menu set " + strings.Join(keys, ",") + " where menu_id = ?" + + // 执行更新 + values = append(values, sysMenu.MenuID) + results, err := datasource.ExecDB("", sql, values) + if err != nil { + log.Errorf("update row : %v", err.Error()) + return 0 + } + affected, err := results.RowsAffected() + if err != nil { + log.Errorf("update err => %v", err) + return 0 + } + return affected +} + +// DeleteMenuById 删除菜单管理信息 +func (r *RepoSysMenu) DeleteMenuById(menuId string) int64 { + sql := "delete from sys_menu where menu_id = ?" + results, err := datasource.ExecDB("", sql, []any{menuId}) + if err != nil { + log.Errorf("delete err => %v", err) + return 0 + } + affected, err := results.RowsAffected() + if err != nil { + log.Errorf("delete err => %v", err) + return 0 + } + return affected +} + +// CheckUniqueMenu 校验菜单是否唯一 +func (r *RepoSysMenu) CheckUniqueMenu(sysMenu model.SysMenu) string { + // 查询条件拼接 + var conditions []string + var params []any + if sysMenu.MenuName != "" { + conditions = append(conditions, "menu_name = ?") + params = append(params, sysMenu.MenuName) + } + if sysMenu.ParentID != "" { + conditions = append(conditions, "parent_id = ?") + params = append(params, sysMenu.ParentID) + } + if sysMenu.Path != "" { + conditions = append(conditions, "path = ?") + params = append(params, sysMenu.Path) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + if whereSql == "" { + return "" + } + + // 查询数据 + querySql := "select menu_id as 'str' from sys_menu " + whereSql + " limit 1" + results, err := datasource.RawDB("", querySql, params) + if err != nil { + log.Errorf("query err %v", err) + return "" + } + if len(results) > 0 { + return fmt.Sprintf("%v", results[0]["str"]) + } + return "" +} diff --git a/features/sys_menu/service/service_sys_menu.go b/features/sys_menu/service/service_sys_menu.go new file mode 100644 index 0000000..3c71d17 --- /dev/null +++ b/features/sys_menu/service/service_sys_menu.go @@ -0,0 +1,398 @@ +package service + +import ( + "encoding/base64" + "strings" + + "ems.agt/features/sys_menu/consts" + "ems.agt/features/sys_menu/model" + sysRoleService "ems.agt/features/sys_role/service" + sysrolemenu "ems.agt/features/sys_role_menu" + "ems.agt/lib/core/utils/parse" + "ems.agt/lib/core/utils/regular" + "ems.agt/lib/core/vo" +) + +// 实例化服务层 ServiceSysMenu 结构体 +var NewServiceSysMenu = &ServiceSysMenu{ + sysMenuRepository: NewRepoSysMenu, + sysRoleMenuRepository: sysrolemenu.NewRepoSysRoleMenu, + sysRoleRepository: sysRoleService.NewRepoSysRole, +} + +// ServiceSysMenu 菜单 服务层处理 +type ServiceSysMenu struct { + // 菜单服务 + sysMenuRepository *RepoSysMenu + // 角色与菜单关联服务 + sysRoleMenuRepository *sysrolemenu.RepoSysRoleMenu + // 角色服务 + sysRoleRepository *sysRoleService.RepoSysRole +} + +// SelectMenuList 查询系统菜单列表 +func (r *ServiceSysMenu) SelectMenuList(sysMenu model.SysMenu, userId string) []model.SysMenu { + return r.sysMenuRepository.SelectMenuList(sysMenu, userId) +} + +// SelectMenuPermsByUserId 根据用户ID查询权限 +func (r *ServiceSysMenu) SelectMenuPermsByUserId(userId string) []string { + return r.sysMenuRepository.SelectMenuPermsByUserId(userId) +} + +// SelectMenuTreeByUserId 根据用户ID查询菜单 +func (r *ServiceSysMenu) SelectMenuTreeByUserId(userId string) []model.SysMenu { + sysMenus := r.sysMenuRepository.SelectMenuTreeByUserId(userId) + return r.parseDataToTree(sysMenus) +} + +// SelectMenuTreeSelectByUserId 根据用户ID查询菜单树结构信息 +func (r *ServiceSysMenu) SelectMenuTreeSelectByUserId(sysMenu model.SysMenu, userId string) []vo.TreeSelect { + sysMenus := r.sysMenuRepository.SelectMenuList(sysMenu, userId) + menus := r.parseDataToTree(sysMenus) + tree := make([]vo.TreeSelect, 0) + for _, menu := range menus { + tree = append(tree, sysMenuTreeSelect(menu)) + } + return tree +} + +// sysMenuTreeSelect 使用给定的 SysMenu 对象解析为 TreeSelect 对象 +func sysMenuTreeSelect(sysMenu model.SysMenu) vo.TreeSelect { + t := vo.TreeSelect{} + t.ID = sysMenu.MenuID + t.Label = sysMenu.MenuName + t.Title = sysMenu.MenuName + + if len(sysMenu.Children) > 0 { + for _, menu := range sysMenu.Children { + child := sysMenuTreeSelect(menu) + t.Children = append(t.Children, child) + } + } else { + t.Children = []vo.TreeSelect{} + } + + return t +} + +// SelectMenuListByRoleId 根据角色ID查询菜单树信息 TODO +func (r *ServiceSysMenu) SelectMenuListByRoleId(roleId string) []string { + roles := r.sysRoleRepository.SelectRoleByIds([]string{roleId}) + if len(roles) > 0 { + role := roles[0] + if role.RoleID == roleId { + return r.sysMenuRepository.SelectMenuListByRoleId( + role.RoleID, + role.MenuCheckStrictly == "1", + ) + } + } + return []string{} +} + +// SelectMenuById 根据菜单ID查询信息 +func (r *ServiceSysMenu) SelectMenuById(menuId string) model.SysMenu { + if menuId == "" { + return model.SysMenu{} + } + menus := r.sysMenuRepository.SelectMenuByIds([]string{menuId}) + if len(menus) > 0 { + return menus[0] + } + return model.SysMenu{} +} + +// HasChildByMenuIdAndStatus 存在菜单子节点数量与状态 +func (r *ServiceSysMenu) HasChildByMenuIdAndStatus(menuId, status string) int64 { + return r.sysMenuRepository.HasChildByMenuIdAndStatus(menuId, status) +} + +// CheckMenuExistRole 查询菜单是否存在角色 +func (r *ServiceSysMenu) CheckMenuExistRole(menuId string) int64 { + return r.sysRoleMenuRepository.CheckMenuExistRole(menuId) +} + +// InsertMenu 新增菜单信息 +func (r *ServiceSysMenu) InsertMenu(sysMenu model.SysMenu) string { + return r.sysMenuRepository.InsertMenu(sysMenu) +} + +// UpdateMenu 修改菜单信息 +func (r *ServiceSysMenu) UpdateMenu(sysMenu model.SysMenu) int64 { + return r.sysMenuRepository.UpdateMenu(sysMenu) +} + +// DeleteMenuById 删除菜单管理信息 +func (r *ServiceSysMenu) DeleteMenuById(menuId string) int64 { + // 删除菜单与角色关联 + r.sysRoleMenuRepository.DeleteMenuRole([]string{menuId}) + return r.sysMenuRepository.DeleteMenuById(menuId) +} + +// CheckUniqueMenuName 校验菜单名称是否唯一 +func (r *ServiceSysMenu) CheckUniqueMenuName(menuName, parentId, menuId string) bool { + uniqueId := r.sysMenuRepository.CheckUniqueMenu(model.SysMenu{ + MenuName: menuName, + ParentID: parentId, + }) + if uniqueId == menuId { + return true + } + return uniqueId == "" +} + +// CheckUniqueMenuPath 校验路由地址是否唯一(针对目录和菜单) +func (r *ServiceSysMenu) CheckUniqueMenuPath(path, menuId string) bool { + uniqueId := r.sysMenuRepository.CheckUniqueMenu(model.SysMenu{ + Path: path, + }) + if uniqueId == menuId { + return true + } + return uniqueId == "" +} + +// BuildRouteMenus 构建前端路由所需要的菜单 +func (r *ServiceSysMenu) BuildRouteMenus(sysMenus []model.SysMenu, prefix string) []vo.Router { + routers := []vo.Router{} + for _, item := range sysMenus { + router := vo.Router{} + router.Name = r.getRouteName(item) + router.Path = r.getRouterPath(item) + router.Component = r.getComponent(item) + router.Meta = r.getRouteMeta(item) + + // 子项菜单 目录类型 非路径链接 + cMenus := item.Children + if len(cMenus) > 0 && item.MenuType == consts.TYPE_DIR && !regular.ValidHttp(item.Path) { + // 获取重定向地址 + redirectPrefix, redirectPath := r.getRouteRedirect( + cMenus, + router.Path, + prefix, + ) + router.Redirect = redirectPath + // 子菜单进入递归 + router.Children = r.BuildRouteMenus(cMenus, redirectPrefix) + } else if item.ParentID == "0" && item.IsFrame == "1" && item.MenuType == consts.TYPE_MENU { + // 父菜单 内部跳转 菜单类型 + menuPath := "/" + item.MenuID + childPath := menuPath + r.getRouterPath(item) + children := vo.Router{ + Name: r.getRouteName(item), + Path: childPath, + Component: item.Component, + Meta: r.getRouteMeta(item), + } + router.Meta.HideChildInMenu = true + router.Children = append(router.Children, children) + router.Name = item.MenuID + router.Path = menuPath + router.Redirect = childPath + router.Component = consts.COMPONENT_LAYOUT_BASIC + } else if item.ParentID == "0" && item.IsFrame == "1" && regular.ValidHttp(item.Path) { + // 父菜单 内部跳转 路径链接 + menuPath := "/" + item.MenuID + childPath := menuPath + r.getRouterPath(item) + children := vo.Router{ + Name: r.getRouteName(item), + Path: childPath, + Component: consts.COMPONENT_LAYOUT_LINK, + Meta: r.getRouteMeta(item), + } + router.Meta.HideChildInMenu = true + router.Children = append(router.Children, children) + router.Name = item.MenuID + router.Path = menuPath + router.Redirect = childPath + router.Component = consts.COMPONENT_LAYOUT_BASIC + } + + routers = append(routers, router) + } + return routers +} + +// getRouteName 获取路由名称 路径英文首字母大写 +func (r *ServiceSysMenu) getRouteName(sysMenu model.SysMenu) string { + routerName := parse.FirstUpper(sysMenu.Path) + // 路径链接 + if regular.ValidHttp(sysMenu.Path) { + return routerName[:5] + "Link" + sysMenu.MenuID + } + return routerName +} + +// getRouterPath 获取路由地址 +func (r *ServiceSysMenu) getRouterPath(sysMenu model.SysMenu) string { + routerPath := sysMenu.Path + + // 显式路径 + if routerPath == "" || strings.HasPrefix(routerPath, "/") { + return routerPath + } + + // 路径链接 内部跳转 + if regular.ValidHttp(routerPath) && sysMenu.IsFrame == "1" { + routerPath = regular.Replace(routerPath, `/^http(s)?:\/\/+/`, "") + routerPath = base64.StdEncoding.EncodeToString([]byte(routerPath)) + } + + // 父菜单 内部跳转 + if sysMenu.ParentID == "0" && sysMenu.IsFrame == "1" { + routerPath = "/" + routerPath + } + + return routerPath +} + +// getComponent 获取组件信息 +func (r *ServiceSysMenu) getComponent(sysMenu model.SysMenu) string { + // 内部跳转 路径链接 + if sysMenu.IsFrame == "1" && regular.ValidHttp(sysMenu.Path) { + return consts.COMPONENT_LAYOUT_LINK + } + + // 非父菜单 目录类型 + if sysMenu.ParentID != "0" && sysMenu.MenuType == consts.TYPE_DIR { + return consts.COMPONENT_LAYOUT_BLANK + } + + // 组件路径 内部跳转 菜单类型 + if sysMenu.Component != "" && sysMenu.IsFrame == "1" && sysMenu.MenuType == consts.TYPE_MENU { + // 父菜单套外层布局 + if sysMenu.ParentID == "0" { + return consts.COMPONENT_LAYOUT_BASIC + } + return sysMenu.Component + } + + return consts.COMPONENT_LAYOUT_BASIC +} + +// getRouteMeta 获取路由元信息 +func (r *ServiceSysMenu) getRouteMeta(sysMenu model.SysMenu) vo.RouterMeta { + meta := vo.RouterMeta{} + if sysMenu.Icon == "#" { + meta.Icon = "" + } else { + meta.Icon = sysMenu.Icon + } + meta.Title = sysMenu.MenuName + meta.HideChildInMenu = false + meta.HideInMenu = sysMenu.Visible == "0" + meta.Cache = sysMenu.IsCache == "1" + meta.Target = "" + + // 路径链接 非内部跳转 + if regular.ValidHttp(sysMenu.Path) && sysMenu.IsFrame == "0" { + meta.Target = "_blank" + } + + return meta +} + +// getRouteRedirect 获取路由重定向地址(针对目录) +// +// cMenus 子菜单数组 +// routerPath 当前菜单路径 +// prefix 菜单重定向路径前缀 +func (r *ServiceSysMenu) getRouteRedirect(cMenus []model.SysMenu, routerPath string, prefix string) (string, string) { + redirectPath := "" + + // 重定向为首个显示并启用的子菜单 + var firstChild *model.SysMenu + for _, item := range cMenus { + if item.IsFrame == "1" && item.Visible == "1" { + firstChild = &item + break + } + } + + // 检查内嵌隐藏菜单是否可做重定向 + if firstChild == nil { + for _, item := range cMenus { + if item.IsFrame == "1" && item.Visible == "1" && strings.Contains(item.Path, consts.PATH_INLINE) { + firstChild = &item + break + } + } + } + + if firstChild != nil { + firstChildPath := r.getRouterPath(*firstChild) + if strings.HasPrefix(firstChildPath, "/") { + redirectPath = firstChildPath + } else { + // 拼接追加路径 + if !strings.HasPrefix(routerPath, "/") { + prefix += "/" + } + prefix = prefix + routerPath + redirectPath = prefix + "/" + firstChildPath + } + } + + return prefix, redirectPath +} + +// parseDataToTree 将数据解析为树结构,构建前端所需要下拉树结构 +func (r *ServiceSysMenu) parseDataToTree(sysMenus []model.SysMenu) []model.SysMenu { + // 节点分组 + nodesMap := make(map[string][]model.SysMenu) + // 节点id + treeIds := []string{} + // 树节点 + tree := []model.SysMenu{} + + for _, item := range sysMenus { + parentID := item.ParentID + // 分组 + mapItem, ok := nodesMap[parentID] + if !ok { + mapItem = []model.SysMenu{} + } + mapItem = append(mapItem, item) + nodesMap[parentID] = mapItem + // 记录节点ID + treeIds = append(treeIds, item.MenuID) + } + + for key, value := range nodesMap { + // 选择不是节点ID的作为树节点 + found := false + for _, id := range treeIds { + if id == key { + found = true + break + } + } + if !found { + tree = append(tree, value...) + } + } + + for i, node := range tree { + iN := r.parseDataToTreeComponet(node, &nodesMap) + tree[i] = iN + } + + return tree +} + +// parseDataToTreeComponet 递归函数处理子节点 +func (r *ServiceSysMenu) parseDataToTreeComponet(node model.SysMenu, nodesMap *map[string][]model.SysMenu) model.SysMenu { + id := node.MenuID + children, ok := (*nodesMap)[id] + if ok { + node.Children = children + } + if len(node.Children) > 0 { + for i, child := range node.Children { + icN := r.parseDataToTreeComponet(child, nodesMap) + node.Children[i] = icN + } + } + return node +} diff --git a/features/sys_role/api_sys_role.go b/features/sys_role/api_sys_role.go new file mode 100644 index 0000000..5ba0582 --- /dev/null +++ b/features/sys_role/api_sys_role.go @@ -0,0 +1,368 @@ +package sysrole + +import ( + "fmt" + "net/http" + "strings" + + "ems.agt/features/sys_role/model" + "ems.agt/features/sys_role/service" + userService "ems.agt/features/sys_user/service" + "ems.agt/lib/core/utils/ctx" + "ems.agt/lib/core/utils/parse" + "ems.agt/lib/core/vo/result" + "ems.agt/lib/midware" + "ems.agt/lib/services" + "ems.agt/restagent/config" +) + +// 角色接口添加到路由 +func Routers() []services.RouterItem { + // 实例化控制层 SysRoleApi 结构体 + var apis = &SysRoleApi{ + sysRoleService: service.NewServiceSysRole, + sysUserService: userService.NewServiceSysUser, + } + + rs := [...]services.RouterItem{ + { + Method: "GET", + Pattern: "/roles", + Handler: apis.List, + Middleware: midware.Authorize(nil), + }, + { + Method: "GET", + Pattern: "/role/{roleId}", + Handler: apis.Info, + Middleware: midware.Authorize(nil), + }, + { + Method: "POST", + Pattern: "/role", + Handler: apis.Add, + Middleware: midware.Authorize(nil), + }, + { + Method: "PUT", + Pattern: "/role", + Handler: apis.Edit, + Middleware: midware.Authorize(nil), + }, + { + Method: "DELETE", + Pattern: "/role/{roleIds}", + Handler: apis.Remove, + Middleware: midware.Authorize(nil), + }, + { + Method: "PUT", + Pattern: "/role/changeStatus", + Handler: apis.Status, + Middleware: midware.Authorize(nil), + }, + { + Method: "GET", + Pattern: "/role/authUser/allocatedList", + Handler: apis.AuthUserAllocatedList, + Middleware: midware.Authorize(nil), + }, + { + Method: "PUT", + Pattern: "/role/authUser/checked", + Handler: apis.AuthUserChecked, + Middleware: midware.Authorize(nil), + }, + // 添加更多的 Router 对象... + } + + // 生成两组前缀路由 + rsPrefix := []services.RouterItem{} + for _, v := range rs { + path := "/roleManage/{apiVersion}" + v.Pattern + // 固定前缀 + v.Pattern = config.DefaultUriPrefix + path + rsPrefix = append(rsPrefix, v) + // 可配置 + v.Pattern = config.UriPrefix + path + rsPrefix = append(rsPrefix, v) + } + return rsPrefix +} + +// // 实例化控制层 SysRoleApi 结构体 +// var NewSysRole = &SysRoleApi{ +// sysRoleService: sysrole.NewServiceSysRole, +// sysUserService: sysuser.NewServiceSysUser, +// } + +// 角色信息 +// +// PATH /roleManage +type SysRoleApi struct { + // 角色服务 + sysRoleService *service.ServiceSysRole + // 用户服务 + sysUserService *userService.ServiceSysUser +} + +// 角色列表 +// +// GET /list +func (s *SysRoleApi) List(w http.ResponseWriter, r *http.Request) { + querys := ctx.QueryMap(r) + data := s.sysRoleService.SelectRolePage(querys) + ctx.JSON(w, 200, result.Ok(data)) +} + +// 角色信息详情 +// +// GET /:roleId +func (s *SysRoleApi) Info(w http.ResponseWriter, r *http.Request) { + roleId := ctx.Param(r, "roleId") + if roleId == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + data := s.sysRoleService.SelectRoleById(roleId) + if data.RoleID == roleId { + ctx.JSON(w, 200, result.OkData(data)) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} + +// 角色信息新增 +// +// POST / +func (s *SysRoleApi) Add(w http.ResponseWriter, r *http.Request) { + var body model.SysRole + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.RoleID != "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + // 判断角色名称是否唯一 + uniqueRoleName := s.sysRoleService.CheckUniqueRoleName(body.RoleName, "") + if !uniqueRoleName { + msg := fmt.Sprintf("角色新增【%s】失败,角色名称已存在", body.RoleName) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + // 判断角色键值是否唯一 + uniqueRoleKey := s.sysRoleService.CheckUniqueRoleKey(body.RoleKey, "") + if !uniqueRoleKey { + msg := fmt.Sprintf("角色新增【%s】失败,角色键值已存在", body.RoleName) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + body.CreateBy = ctx.LoginUserToUserName(r) + insertId := s.sysRoleService.InsertRole(body) + if insertId != "" { + ctx.JSON(w, 200, result.Ok(nil)) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} + +// 角色信息修改 +// +// PUT / +func (s *SysRoleApi) Edit(w http.ResponseWriter, r *http.Request) { + var body model.SysRole + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.RoleID == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + // 检查是否管理员角色 + if body.RoleID == "1" { + ctx.JSON(w, 200, result.ErrMsg("不允许操作管理员角色")) + return + } + + // 检查是否存在 + role := s.sysRoleService.SelectRoleById(body.RoleID) + if role.RoleID != body.RoleID { + ctx.JSON(w, 200, result.ErrMsg("没有权限访问角色数据!")) + return + } + + // 判断角色名称是否唯一 + uniqueRoleName := s.sysRoleService.CheckUniqueRoleName(body.RoleName, body.RoleID) + if !uniqueRoleName { + msg := fmt.Sprintf("角色修改【%s】失败,角色名称已存在", body.RoleName) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + // 判断角色键值是否唯一 + uniqueRoleKey := s.sysRoleService.CheckUniqueRoleKey(body.RoleKey, body.RoleID) + if !uniqueRoleKey { + msg := fmt.Sprintf("角色修改【%s】失败,角色键值已存在", body.RoleName) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + body.UpdateBy = ctx.LoginUserToUserName(r) + rows := s.sysRoleService.UpdateRole(body) + if rows > 0 { + ctx.JSON(w, 200, result.Ok(nil)) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} + +// 角色信息删除 +// +// DELETE /:roleIds +func (s *SysRoleApi) Remove(w http.ResponseWriter, r *http.Request) { + roleIds := ctx.Param(r, "roleIds") + if roleIds == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + // 处理字符转id数组后去重 + ids := strings.Split(roleIds, ",") + uniqueIDs := parse.RemoveDuplicates(ids) + if len(uniqueIDs) <= 0 { + ctx.JSON(w, 200, result.Err(nil)) + return + } + // 检查是否管理员角色 + for _, id := range uniqueIDs { + if id == "1" { + ctx.JSON(w, 200, result.ErrMsg("不允许操作管理员角色")) + return + } + } + rows, err := s.sysRoleService.DeleteRoleByIds(uniqueIDs) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + msg := fmt.Sprintf("删除成功:%d", rows) + ctx.JSON(w, 200, result.OkMsg(msg)) +} + +// 角色状态变更 +// +// PUT /changeStatus +func (s *SysRoleApi) Status(w http.ResponseWriter, r *http.Request) { + var body struct { + // 角色ID + RoleID string `json:"roleId" binding:"required"` + // 状态 + Status string `json:"status" binding:"required"` + } + err := ctx.ShouldBindJSON(r, &body) + if err != nil { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + // 检查是否管理员角色 + if body.RoleID == "1" { + ctx.JSON(w, 200, result.ErrMsg("不允许操作管理员角色")) + return + } + + // 检查是否存在 + role := s.sysRoleService.SelectRoleById(body.RoleID) + if role.RoleID != body.RoleID { + ctx.JSON(w, 200, result.ErrMsg("没有权限访问角色数据!")) + return + } + + // 与旧值相等不变更 + if role.Status == body.Status { + ctx.JSON(w, 200, result.ErrMsg("变更状态与旧值相等!")) + return + } + + // 更新状态不刷新缓存 + userName := ctx.LoginUserToUserName(r) + SysRoleApi := model.SysRole{ + RoleID: body.RoleID, + Status: body.Status, + UpdateBy: userName, + } + rows := s.sysRoleService.UpdateRole(SysRoleApi) + if rows > 0 { + ctx.JSON(w, 200, result.Ok(nil)) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} + +// 角色分配用户列表 +// +// GET /authUser/allocatedList +func (s *SysRoleApi) AuthUserAllocatedList(w http.ResponseWriter, r *http.Request) { + querys := ctx.QueryMap(r) + roleId, ok := querys["roleId"] + if !ok || roleId == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + // 检查是否存在 + role := s.sysRoleService.SelectRoleById(roleId.(string)) + if role.RoleID != roleId { + ctx.JSON(w, 200, result.ErrMsg("没有权限访问角色数据!")) + return + } + + data := s.sysUserService.SelectAllocatedPage(querys) + ctx.JSON(w, 200, result.Ok(data)) +} + +// 角色分配选择授权 +// +// PUT /authUser/checked +func (s *SysRoleApi) AuthUserChecked(w http.ResponseWriter, r *http.Request) { + var body struct { + // 角色ID + RoleID string `json:"roleId" binding:"required"` + // 用户ID组 + UserIDs string `json:"userIds" binding:"required"` + // 选择操作 添加true 取消false + Checked bool `json:"checked"` + } + err := ctx.ShouldBindJSON(r, &body) + if err != nil { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + // 处理字符转id数组后去重 + ids := strings.Split(body.UserIDs, ",") + uniqueIDs := parse.RemoveDuplicates(ids) + if len(uniqueIDs) <= 0 { + ctx.JSON(w, 200, result.Err(nil)) + return + } + + // 检查是否存在 + role := s.sysRoleService.SelectRoleById(body.RoleID) + if role.RoleID != body.RoleID { + ctx.JSON(w, 200, result.ErrMsg("没有权限访问角色数据!")) + return + } + + var rows int64 + if body.Checked { + rows = s.sysRoleService.InsertAuthUsers(body.RoleID, uniqueIDs) + } else { + rows = s.sysRoleService.DeleteAuthUsers(body.RoleID, uniqueIDs) + } + if rows > 0 { + ctx.JSON(w, 200, result.Ok(nil)) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} diff --git a/features/sys_role/model/sys_role.go b/features/sys_role/model/sys_role.go new file mode 100644 index 0000000..921fd25 --- /dev/null +++ b/features/sys_role/model/sys_role.go @@ -0,0 +1,38 @@ +package model + +// SysRole 角色对象 sys_role +type SysRole struct { + // 角色ID + RoleID string `json:"roleId"` + // 角色名称 + RoleName string `json:"roleName" binding:"required"` + // 角色键值 + RoleKey string `json:"roleKey" binding:"required"` + // 显示顺序 + RoleSort int `json:"roleSort"` + // 菜单树选择项是否关联显示(0:父子不互相关联显示 1:父子互相关联显示) + MenuCheckStrictly string `json:"menuCheckStrictly"` + // 部门树选择项是否关联显示(0:父子不互相关联显示 1:父子互相关联显示) + DeptCheckStrictly string `json:"deptCheckStrictly"` + // 角色状态(0停用 1正常) + Status string `json:"status"` + // 删除标志(0代表存在 1代表删除) + DelFlag string `json:"delFlag"` + // 创建者 + CreateBy string `json:"createBy"` + // 创建时间 + CreateTime int64 `json:"createTime"` + // 更新者 + UpdateBy string `json:"updateBy"` + // 更新时间 + UpdateTime int64 `json:"updateTime"` + // 备注 + Remark string `json:"remark"` + + // ====== 非数据库字段属性 ====== + + // 菜单组 + MenuIds []string `json:"menuIds,omitempty"` + // 部门组(数据权限) + DeptIds []string `json:"deptIds,omitempty"` +} diff --git a/features/sys_role/service/repo_sys_role.go b/features/sys_role/service/repo_sys_role.go new file mode 100644 index 0000000..7f61e8a --- /dev/null +++ b/features/sys_role/service/repo_sys_role.go @@ -0,0 +1,362 @@ +package service + +import ( + "fmt" + "strings" + "time" + + "ems.agt/features/sys_role/model" + "ems.agt/lib/core/datasource" + "ems.agt/lib/core/utils/date" + "ems.agt/lib/core/utils/parse" + "ems.agt/lib/log" +) + +// 实例化数据层 RepoSysRole 结构体 +var NewRepoSysRole = &RepoSysRole{ + selectSql: `select distinct + r.role_id, r.role_name, r.role_key, r.role_sort, r.menu_check_strictly, + r.dept_check_strictly, r.status, r.del_flag, r.create_time, r.remark + from sys_role r + left join sys_user_role ur on ur.role_id = r.role_id + left join user u on u.id = ur.user_id`, + + resultMap: map[string]string{ + "role_id": "RoleID", + "role_name": "RoleName", + "role_key": "RoleKey", + "role_sort": "RoleSort", + "menu_check_strictly": "MenuCheckStrictly", + "dept_check_strictly": "DeptCheckStrictly", + "status": "Status", + "del_flag": "DelFlag", + "create_by": "CreateBy", + "create_time": "CreateTime", + "update_by": "UpdateBy", + "update_time": "UpdateTime", + "remark": "Remark", + }, +} + +// RepoSysRole 角色表 数据层处理 +type RepoSysRole struct { + // 查询视图对象SQL + selectSql string + // 结果字段与实体映射 + resultMap map[string]string +} + +// convertResultRows 将结果记录转实体结果组 +func (r *RepoSysRole) convertResultRows(rows []map[string]any) []model.SysRole { + arr := make([]model.SysRole, 0) + for _, row := range rows { + sysRole := model.SysRole{} + for key, value := range row { + if keyMapper, ok := r.resultMap[key]; ok { + datasource.SetFieldValue(&sysRole, keyMapper, value) + } + } + arr = append(arr, sysRole) + } + return arr +} + +// SelectRolePage 根据条件分页查询角色数据 +func (r *RepoSysRole) SelectRolePage(query map[string]any) map[string]any { + // 查询条件拼接 + var conditions []string + var params []any + if v, ok := query["roleId"]; ok && v != "" { + conditions = append(conditions, "r.role_id = ?") + params = append(params, v) + } + if v, ok := query["roleName"]; ok && v != "" { + conditions = append(conditions, "r.role_name like concat('%', concat(?, '%'))") + params = append(params, v) + } + if v, ok := query["roleKey"]; ok && v != "" { + conditions = append(conditions, "r.role_key like concat('%', concat(?, '%'))") + params = append(params, v) + } + if v, ok := query["status"]; ok && v != "" { + conditions = append(conditions, "r.status = ?") + params = append(params, v) + } + beginTime, ok := query["beginTime"] + if !ok { + beginTime, ok = query["params[beginTime]"] + } + if ok && beginTime != "" { + conditions = append(conditions, "r.create_time >= ?") + beginDate := date.ParseStrToDate(beginTime.(string), date.YYYY_MM_DD) + params = append(params, beginDate.UnixMilli()) + } + endTime, ok := query["endTime"] + if !ok { + endTime, ok = query["params[endTime]"] + } + if ok && endTime != "" { + conditions = append(conditions, "r.create_time <= ?") + endDate := date.ParseStrToDate(endTime.(string), date.YYYY_MM_DD) + params = append(params, endDate.UnixMilli()) + } + + // 构建查询条件语句 + whereSql := " where r.del_flag = '0' " + if len(conditions) > 0 { + whereSql += " and " + strings.Join(conditions, " and ") + } + + // 查询数量 长度为0直接返回 + totalSql := `select count(distinct r.role_id) as 'total' from sys_role r + left join sys_user_role ur on ur.role_id = r.role_id + left join user u on u.id = ur.user_id` + totalRows, err := datasource.RawDB("", totalSql+whereSql, params) + if err != nil { + log.Errorf("total err => %v", err) + } + total := parse.Number(totalRows[0]["total"]) + if total == 0 { + return map[string]any{ + "total": total, + "rows": []model.SysRole{}, + } + } + + // 分页 + pageNum, pageSize := datasource.PageNumSize(query["pageNum"], query["pageSize"]) + pageSql := " order by r.role_sort asc limit ?,? " + params = append(params, pageNum*pageSize) + params = append(params, pageSize) + + // 查询数据 + querySql := r.selectSql + whereSql + pageSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + log.Errorf("query err => %v", err) + } + + // 转换实体 + rows := r.convertResultRows(results) + return map[string]any{ + "total": total, + "rows": rows, + } +} + +// SelectRoleList 根据条件查询角色数据 +func (r *RepoSysRole) SelectRoleList(sysRole model.SysRole) []model.SysRole { + // 查询条件拼接 + var conditions []string + var params []any + if sysRole.RoleID != "" { + conditions = append(conditions, "r.role_id = ?") + params = append(params, sysRole.RoleID) + } + if sysRole.RoleKey != "" { + conditions = append(conditions, "r.role_key like concat('%', concat(?, '%'))") + params = append(params, sysRole.RoleKey) + } + if sysRole.RoleName != "" { + conditions = append(conditions, "r.role_name like concat('%', concat(?, '%'))") + params = append(params, sysRole.RoleName) + } + if sysRole.Status != "" { + conditions = append(conditions, "r.status = ?") + params = append(params, sysRole.Status) + } + + // 构建查询条件语句 + whereSql := " where r.del_flag = '0' " + if len(conditions) > 0 { + whereSql += " and " + strings.Join(conditions, " and ") + } + + // 查询数据 + orderSql := " order by r.role_sort" + querySql := r.selectSql + whereSql + orderSql + rows, err := datasource.RawDB("", querySql, params) + if err != nil { + log.Errorf("query err => %v", err) + return []model.SysRole{} + } + return r.convertResultRows(rows) +} + +// SelectRoleListByUserId 根据用户ID获取角色选择框列表 +func (r *RepoSysRole) SelectRoleListByUserId(userId string) []model.SysRole { + querySql := r.selectSql + " where r.del_flag = '0' and ur.user_id = ?" + results, err := datasource.RawDB("", querySql, []any{userId}) + if err != nil { + log.Errorf("query err => %v", err) + return []model.SysRole{} + } + return r.convertResultRows(results) +} + +// SelectRoleByIds 通过角色ID查询角色 +func (r *RepoSysRole) SelectRoleByIds(roleIds []string) []model.SysRole { + placeholder := datasource.KeyPlaceholderByQuery(len(roleIds)) + querySql := r.selectSql + " where r.role_id in (" + placeholder + ")" + parameters := datasource.ConvertIdsSlice(roleIds) + results, err := datasource.RawDB("", querySql, parameters) + if err != nil { + log.Errorf("query err => %v", err) + return []model.SysRole{} + } + // 转换实体 + return r.convertResultRows(results) +} + +// UpdateRole 修改角色信息 +func (r *RepoSysRole) UpdateRole(sysRole model.SysRole) int64 { + // 参数拼接 + params := make(map[string]any) + if sysRole.RoleName != "" { + params["role_name"] = sysRole.RoleName + } + if sysRole.RoleKey != "" { + params["role_key"] = sysRole.RoleKey + } + if sysRole.RoleSort > 0 { + params["role_sort"] = sysRole.RoleSort + } + if sysRole.MenuCheckStrictly != "" { + params["menu_check_strictly"] = sysRole.MenuCheckStrictly + } + if sysRole.DeptCheckStrictly != "" { + params["dept_check_strictly"] = sysRole.DeptCheckStrictly + } + if sysRole.Status != "" { + params["status"] = sysRole.Status + } + if sysRole.Remark != "" { + params["remark"] = sysRole.Remark + } + if sysRole.UpdateBy != "" { + params["update_by"] = sysRole.UpdateBy + params["update_time"] = time.Now().UnixMilli() + } + + // 构建执行语句 + keys, values := datasource.KeyValueByUpdate(params) + sql := "update sys_role set " + strings.Join(keys, ",") + " where role_id = ?" + + // 执行更新 + values = append(values, sysRole.RoleID) + results, err := datasource.ExecDB("", sql, values) + if err != nil { + log.Errorf("update row : %v", err.Error()) + return 0 + } + affected, err := results.RowsAffected() + if err != nil { + log.Errorf("update err => %v", err) + return 0 + } + return affected +} + +// InsertRole 新增角色信息 +func (r *RepoSysRole) InsertRole(sysRole model.SysRole) string { + // 参数拼接 + params := make(map[string]any) + if sysRole.RoleID != "" { + params["role_id"] = sysRole.RoleID + } + if sysRole.RoleName != "" { + params["role_name"] = sysRole.RoleName + } + if sysRole.RoleKey != "" { + params["role_key"] = sysRole.RoleKey + } + if sysRole.RoleSort > 0 { + params["role_sort"] = sysRole.RoleSort + } + if sysRole.MenuCheckStrictly != "" { + params["menu_check_strictly"] = sysRole.MenuCheckStrictly + } + if sysRole.DeptCheckStrictly != "" { + params["dept_check_strictly"] = sysRole.DeptCheckStrictly + } + if sysRole.Status != "" { + params["status"] = sysRole.Status + } + if sysRole.Remark != "" { + params["remark"] = sysRole.Remark + } + if sysRole.CreateBy != "" { + params["create_by"] = sysRole.CreateBy + params["create_time"] = time.Now().UnixMilli() + } + + // 构建执行语句 + keys, placeholder, values := datasource.KeyPlaceholderValueByInsert(params) + sql := "insert into sys_role (" + strings.Join(keys, ",") + ")values(" + placeholder + ")" + + // 执行插入 + results, err := datasource.ExecDB("", sql, values) + if err != nil { + log.Errorf("insert row : %v", err.Error()) + return "" + } + insertId, err := results.LastInsertId() + if err != nil { + log.Errorf("insert row : %v", err.Error()) + return "" + } + return fmt.Sprint(insertId) +} + +// DeleteRoleByIds 批量删除角色信息 +func (r *RepoSysRole) DeleteRoleByIds(roleIds []string) int64 { + placeholder := datasource.KeyPlaceholderByQuery(len(roleIds)) + sql := "update sys_role set del_flag = '1' where role_id in (" + placeholder + ")" + parameters := datasource.ConvertIdsSlice(roleIds) + results, err := datasource.ExecDB("", sql, parameters) + if err != nil { + log.Errorf("delete err => %v", err) + return 0 + } + affected, err := results.RowsAffected() + if err != nil { + log.Errorf("delete err => %v", err) + return 0 + } + return affected +} + +// CheckUniqueRole 校验角色是否唯一 +func (r *RepoSysRole) CheckUniqueRole(sysRole model.SysRole) string { + // 查询条件拼接 + var conditions []string + var params []any + if sysRole.RoleName != "" { + conditions = append(conditions, "r.role_name = ?") + params = append(params, sysRole.RoleName) + } + if sysRole.RoleKey != "" { + conditions = append(conditions, "r.role_key = ?") + params = append(params, sysRole.RoleKey) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } else { + return "" + } + + // 查询数据 + querySql := "select role_id as 'str' from sys_role r " + whereSql + " and r.del_flag = '0' limit 1" + results, err := datasource.RawDB("", querySql, params) + if err != nil { + log.Errorf("query err %v", err) + return "" + } + if len(results) > 0 { + return fmt.Sprintf("%v", results[0]["str"]) + } + return "" +} diff --git a/features/sys_role/service/service_sys_role.go b/features/sys_role/service/service_sys_role.go new file mode 100644 index 0000000..187d0c0 --- /dev/null +++ b/features/sys_role/service/service_sys_role.go @@ -0,0 +1,165 @@ +package service + +import ( + "errors" + "fmt" + + "ems.agt/features/sys_role/model" + sysrolemenu "ems.agt/features/sys_role_menu" + sysuserrole "ems.agt/features/sys_user_role" +) + +// 实例化服务层 ServiceSysRole 结构体 +var NewServiceSysRole = &ServiceSysRole{ + sysRoleRepository: NewRepoSysRole, + sysUserRoleRepository: sysuserrole.NewRepoSysUserRole, + sysRoleMenuRepository: sysrolemenu.NewRepoSysRoleMenu, +} + +// ServiceSysRole 角色 服务层处理 +type ServiceSysRole struct { + // 角色服务 + sysRoleRepository *RepoSysRole + // 用户与角色关联服务 + sysUserRoleRepository *sysuserrole.RepoSysUserRole + // 角色与菜单关联服务 + sysRoleMenuRepository *sysrolemenu.RepoSysRoleMenu +} + +// SelectRolePage 根据条件分页查询角色数据 +func (r *ServiceSysRole) SelectRolePage(query map[string]any) map[string]any { + return r.sysRoleRepository.SelectRolePage(query) +} + +// SelectRoleList 根据条件查询角色数据 +func (r *ServiceSysRole) SelectRoleList(sysRole model.SysRole) []model.SysRole { + return r.sysRoleRepository.SelectRoleList(sysRole) +} + +// SelectRoleListByUserId 根据用户ID获取角色选择框列表 +func (r *ServiceSysRole) SelectRoleListByUserId(userId string) []model.SysRole { + return r.sysRoleRepository.SelectRoleListByUserId(userId) +} + +// SelectRoleById 通过角色ID查询角色 +func (r *ServiceSysRole) SelectRoleById(roleId string) model.SysRole { + if roleId == "" { + return model.SysRole{} + } + posts := r.sysRoleRepository.SelectRoleByIds([]string{roleId}) + if len(posts) > 0 { + return posts[0] + } + return model.SysRole{} +} + +// UpdateRole 修改角色信息 +func (r *ServiceSysRole) UpdateRole(sysRole model.SysRole) int64 { + rows := r.sysRoleRepository.UpdateRole(sysRole) + if rows > 0 { + // 删除角色与菜单关联 + r.sysRoleMenuRepository.DeleteRoleMenu([]string{sysRole.RoleID}) + if len(sysRole.MenuIds) > 0 { + r.insertRoleMenu(sysRole.RoleID, sysRole.MenuIds) + } + } + return rows +} + +// InsertRole 新增角色信息 +func (r *ServiceSysRole) InsertRole(sysRole model.SysRole) string { + insertId := r.sysRoleRepository.InsertRole(sysRole) + if insertId != "" && len(sysRole.MenuIds) > 0 { + r.insertRoleMenu(insertId, sysRole.MenuIds) + } + return insertId +} + +// insertRoleMenu 新增角色菜单信息 +func (r *ServiceSysRole) insertRoleMenu(roleId string, menuIds []string) int64 { + if roleId == "" || len(menuIds) <= 0 { + return 0 + } + + sysRoleMenus := []sysrolemenu.SysRoleMenu{} + for _, menuId := range menuIds { + if menuId == "" { + continue + } + sysRoleMenus = append(sysRoleMenus, sysrolemenu.NewSysRoleMenu(roleId, menuId)) + } + + return r.sysRoleMenuRepository.BatchRoleMenu(sysRoleMenus) +} + +// DeleteRoleByIds 批量删除角色信息 +func (r *ServiceSysRole) DeleteRoleByIds(roleIds []string) (int64, error) { + // 检查是否存在 + roles := r.sysRoleRepository.SelectRoleByIds(roleIds) + if len(roles) <= 0 { + return 0, errors.New("没有权限访问角色数据!") + } + for _, role := range roles { + // 检查是否为已删除 + if role.DelFlag == "1" { + return 0, errors.New(role.RoleID + " 角色信息已经删除!") + } + // 检查分配用户 + userCount := r.sysUserRoleRepository.CountUserRoleByRoleId(role.RoleID) + if userCount > 0 { + msg := fmt.Sprintf("【%s】已分配给用户,不能删除", role.RoleName) + return 0, errors.New(msg) + } + } + if len(roles) == len(roleIds) { + // 删除角色与菜单关联 + r.sysRoleMenuRepository.DeleteRoleMenu(roleIds) + rows := r.sysRoleRepository.DeleteRoleByIds(roleIds) + return rows, nil + } + return 0, errors.New("删除角色信息失败!") +} + +// CheckUniqueRoleName 校验角色名称是否唯一 +func (r *ServiceSysRole) CheckUniqueRoleName(roleName, roleId string) bool { + uniqueId := r.sysRoleRepository.CheckUniqueRole(model.SysRole{ + RoleName: roleName, + }) + if uniqueId == roleId { + return true + } + return uniqueId == "" +} + +// CheckUniqueRoleKey 校验角色权限是否唯一 +func (r *ServiceSysRole) CheckUniqueRoleKey(roleKey, roleId string) bool { + uniqueId := r.sysRoleRepository.CheckUniqueRole(model.SysRole{ + RoleKey: roleKey, + }) + if uniqueId == roleId { + return true + } + return uniqueId == "" +} + +// DeleteAuthUsers 批量取消授权用户角色 +func (r *ServiceSysRole) DeleteAuthUsers(roleId string, userIds []string) int64 { + return r.sysUserRoleRepository.DeleteUserRoleByRoleId(roleId, userIds) +} + +// InsertAuthUsers 批量新增授权用户角色 +func (r *ServiceSysRole) InsertAuthUsers(roleId string, userIds []string) int64 { + if roleId == "" || len(userIds) <= 0 { + return 0 + } + + sysUserRoles := []sysuserrole.SysUserRole{} + for _, userId := range userIds { + if userId == "" { + continue + } + sysUserRoles = append(sysUserRoles, sysuserrole.NewSysUserRole(userId, roleId)) + } + + return r.sysUserRoleRepository.BatchUserRole(sysUserRoles) +} diff --git a/features/sys_role_menu/model_sys_role_menu.go b/features/sys_role_menu/model_sys_role_menu.go new file mode 100644 index 0000000..755e66d --- /dev/null +++ b/features/sys_role_menu/model_sys_role_menu.go @@ -0,0 +1,15 @@ +package sysrolemenu + +// SysRoleMenu 角色和菜单关联对象 sys_role_menu +type SysRoleMenu struct { + RoleID string `json:"roleId"` // 角色ID + MenuID string `json:"menuId"` // 菜单ID +} + +// NewSysRoleMenu 创建角色和菜单关联对象的构造函数 +func NewSysRoleMenu(roleID string, menuID string) SysRoleMenu { + return SysRoleMenu{ + RoleID: roleID, + MenuID: menuID, + } +} diff --git a/features/sys_role_menu/repo_sys_role_menu.go b/features/sys_role_menu/repo_sys_role_menu.go new file mode 100644 index 0000000..02dc826 --- /dev/null +++ b/features/sys_role_menu/repo_sys_role_menu.go @@ -0,0 +1,86 @@ +package sysrolemenu + +import ( + "fmt" + "strings" + + "ems.agt/lib/core/datasource" + "ems.agt/lib/core/utils/parse" + "ems.agt/lib/log" +) + +// 实例化数据层 RepoSysRoleMenu 结构体 +var NewRepoSysRoleMenu = &RepoSysRoleMenu{} + +// RepoSysRoleMenu 角色与菜单关联表 数据层处理 +type RepoSysRoleMenu struct{} + +// CheckMenuExistRole 查询菜单分配给角色使用数量 +func (r *RepoSysRoleMenu) CheckMenuExistRole(menuId string) int64 { + querySql := "select count(1) as 'total' from sys_role_menu where menu_id = ?" + results, err := datasource.RawDB("", querySql, []any{menuId}) + if err != nil { + log.Errorf("query err => %v", err) + return 0 + } + if len(results) > 0 { + return parse.Number(results[0]["total"]) + } + return 0 +} + +// DeleteRoleMenu 批量删除角色和菜单关联 +func (r *RepoSysRoleMenu) DeleteRoleMenu(roleIds []string) int64 { + placeholder := datasource.KeyPlaceholderByQuery(len(roleIds)) + sql := "delete from sys_role_menu where role_id in (" + placeholder + ")" + parameters := datasource.ConvertIdsSlice(roleIds) + results, err := datasource.ExecDB("", sql, parameters) + if err != nil { + log.Errorf("delete err => %v", err) + return 0 + } + affected, err := results.RowsAffected() + if err != nil { + log.Errorf("delete err => %v", err) + return 0 + } + return affected +} + +// DeleteMenuRole 批量删除菜单和角色关联 +func (r *RepoSysRoleMenu) DeleteMenuRole(menuIds []string) int64 { + placeholder := datasource.KeyPlaceholderByQuery(len(menuIds)) + sql := "delete from sys_role_menu where menu_id in (" + placeholder + ")" + parameters := datasource.ConvertIdsSlice(menuIds) + results, err := datasource.ExecDB("", sql, parameters) + if err != nil { + log.Errorf("delete err => %v", err) + return 0 + } + affected, err := results.RowsAffected() + if err != nil { + log.Errorf("delete err => %v", err) + return 0 + } + return affected +} + +// BatchRoleMenu 批量新增角色菜单信息 +func (r *RepoSysRoleMenu) BatchRoleMenu(sysRoleMenus []SysRoleMenu) int64 { + keyValues := make([]string, 0) + for _, item := range sysRoleMenus { + keyValues = append(keyValues, fmt.Sprintf("(%s,%s)", item.RoleID, item.MenuID)) + } + sql := "insert into sys_role_menu(role_id, menu_id) values " + strings.Join(keyValues, ",") + results, err := datasource.ExecDB("", sql, nil) + if err != nil { + log.Errorf("insert err => %v", err) + return 0 + } + insertId, err := results.LastInsertId() + if err != nil { + log.Errorf("insert row : %v", err.Error()) + return 0 + } + return insertId +} diff --git a/features/sys_user/api_sys_user.go b/features/sys_user/api_sys_user.go new file mode 100644 index 0000000..b16a5b5 --- /dev/null +++ b/features/sys_user/api_sys_user.go @@ -0,0 +1,323 @@ +package sysuser + +import ( + "fmt" + "net/http" + "strings" + + sysRoleModel "ems.agt/features/sys_role/model" + sysRoleService "ems.agt/features/sys_role/service" + sysUserModel "ems.agt/features/sys_user/model" + "ems.agt/features/sys_user/service" + "ems.agt/lib/core/conf" + "ems.agt/lib/core/utils/ctx" + "ems.agt/lib/core/utils/parse" + "ems.agt/lib/core/vo/result" + "ems.agt/lib/midware" + "ems.agt/lib/services" + "ems.agt/restagent/config" +) + +// 用户接口添加到路由 +func Routers() []services.RouterItem { + // 实例化控制层 SysUserApi 结构体 + var apis = &SysUserApi{ + sysUserService: service.NewServiceSysUser, + sysRoleService: sysRoleService.NewServiceSysRole, + } + + rs := [...]services.RouterItem{ + { + Method: "GET", + Pattern: "/users", + Handler: apis.List, + Middleware: midware.Authorize(nil), + }, + { + Method: "GET", + Pattern: "/user/{userId}", + Handler: apis.Info, + Middleware: midware.Authorize(nil), + }, + { + Method: "POST", + Pattern: "/user", + Handler: apis.Add, + Middleware: midware.Authorize(nil), + }, + { + Method: "PUT", + Pattern: "/user", + Handler: apis.Edit, + Middleware: midware.Authorize(nil), + }, + { + Method: "DELETE", + Pattern: "/user/{userIds}", + Handler: apis.Remove, + Middleware: midware.Authorize(nil), + }, + // 添加更多的 Router 对象... + } + + // 生成两组前缀路由 + rsPrefix := []services.RouterItem{} + for _, v := range rs { + path := "/userManage/{apiVersion}" + v.Pattern + // 固定前缀 + v.Pattern = config.DefaultUriPrefix + path + rsPrefix = append(rsPrefix, v) + // 可配置 + v.Pattern = config.UriPrefix + path + rsPrefix = append(rsPrefix, v) + } + return rsPrefix +} + +// // 实例化控制层 SysUserApi 结构体 +// var NewSysUser = &SysUserApi{ +// sysUserService: service.NewSysUserImpl, +// sysRoleService: service.NewSysRoleImpl, +// sysPostService: service.NewSysPostImpl, +// sysDictDataService: service.NewSysDictDataImpl, +// } + +// 用户信息 +// +// PATH /system/user +type SysUserApi struct { + // 用户服务 + sysUserService *service.ServiceSysUser + // 角色服务 + sysRoleService *sysRoleService.ServiceSysRole +} + +// 用户信息列表 +// +// GET /list +func (s *SysUserApi) List(w http.ResponseWriter, r *http.Request) { + querys := ctx.QueryMap(r) + data := s.sysUserService.SelectUserPage(querys) + ctx.JSON(w, 200, result.Ok(data)) +} + +// 用户信息详情 +// +// GET /:userId +func (s *SysUserApi) Info(w http.ResponseWriter, r *http.Request) { + userId := ctx.Param(r, "userId") + if userId == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + // 查询系统角色列表 + roles := s.sysRoleService.SelectRoleList(sysRoleModel.SysRole{}) + + // 不是系统指定管理员需要排除其角色 + if !conf.IsAdmin(userId) { + rolesFilter := make([]sysRoleModel.SysRole, 0) + for _, r := range roles { + if r.RoleID != "1" { + rolesFilter = append(rolesFilter, r) + } + } + roles = rolesFilter + } + + // 新增用户时,用户ID为0 + if userId == "0" { + ctx.JSON(w, 200, result.OkData(map[string]any{ + "user": map[string]any{}, + "roleIds": []string{}, + "roles": roles, + })) + return + } + + // 检查用户是否存在 + user := s.sysUserService.SelectUserById(userId) + if user.Id != userId { + ctx.JSON(w, 200, result.ErrMsg("没有权限访问用户数据!")) + return + } + + // 角色ID组 + roleIds := make([]string, 0) + for _, r := range user.Roles { + roleIds = append(roleIds, r.RoleID) + } + + ctx.JSON(w, 200, result.OkData(map[string]any{ + "user": user, + "roleIds": roleIds, + "roles": roles, + })) +} + +// 用户信息新增 +// +// POST / +func (s *SysUserApi) Add(w http.ResponseWriter, r *http.Request) { + var body sysUserModel.SysUser + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.Id != "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + // 检查用户登录账号是否唯一 + uniqueUserName := s.sysUserService.CheckUniqueUserName(body.AccountId, "") + if !uniqueUserName { + msg := fmt.Sprintf("新增用户【%s】失败,登录账号已存在", body.AccountId) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + insertId := s.sysUserService.InsertUser(body) + if insertId != "" { + ctx.JSON(w, 200, result.Ok(nil)) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} + +// 用户信息修改 +// +// POST / +func (s *SysUserApi) Edit(w http.ResponseWriter, r *http.Request) { + var body sysUserModel.SysUser + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.Id == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + // 检查是否管理员用户 + // if conf.IsAdmin(body.Id) { + // ctx.JSON(w, 200, result.ErrMsg("不允许操作管理员用户")) + // return + // } + + user := s.sysUserService.SelectUserById(body.Id) + if user.Id != body.Id { + ctx.JSON(w, 200, result.ErrMsg("没有权限访问用户数据!")) + return + } + + // 检查用户登录账号是否唯一 + uniqueUserName := s.sysUserService.CheckUniqueUserName(body.AccountId, body.Id) + if !uniqueUserName { + msg := fmt.Sprintf("修改用户【%s】失败,登录账号已存在", body.AccountId) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + body.AccountId = "" // 忽略修改登录用户名称 + // body.Password = "" // 忽略修改密码 + rows := s.sysUserService.UpdateUserAndRolePost(body) + if rows > 0 { + ctx.JSON(w, 200, result.Ok(nil)) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} + +// 用户信息删除 +// +// DELETE /:userIds +func (s *SysUserApi) Remove(w http.ResponseWriter, r *http.Request) { + userIds := ctx.Param(r, "userIds") + if userIds == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + // 处理字符转id数组后去重 + ids := strings.Split(userIds, ",") + uniqueIDs := parse.RemoveDuplicates(ids) + if len(uniqueIDs) <= 0 { + ctx.JSON(w, 200, result.Err(nil)) + return + } + rows, err := s.sysUserService.DeleteUserByIds(uniqueIDs) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + msg := fmt.Sprintf("删除成功:%d", rows) + ctx.JSON(w, 200, result.OkMsg(msg)) +} + +// 用户重置密码 +// +// PUT /resetPwd +func (s *SysUserApi) ResetPwd(w http.ResponseWriter, r *http.Request) { + var body struct { + UserID string `json:"userId" binding:"required"` + Password string `json:"password" binding:"required"` + } + if err := ctx.ShouldBindJSON(r, &body); err != nil { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + // 检查是否管理员用户 + if conf.IsAdmin(body.UserID) { + ctx.JSON(w, 200, result.ErrMsg("不允许操作管理员用户")) + return + } + + user := s.sysUserService.SelectUserById(body.UserID) + if user.Id != body.UserID { + ctx.JSON(w, 200, result.ErrMsg("没有权限访问用户数据!")) + return + } + + SysUserApi := sysUserModel.SysUser{ + Id: body.UserID, + Password: body.Password, + } + rows := s.sysUserService.UpdateUser(SysUserApi) + if rows > 0 { + ctx.JSON(w, 200, result.Ok(nil)) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} + +// 用户状态修改 +// +// PUT /changeStatus +func (s *SysUserApi) Status(w http.ResponseWriter, r *http.Request) { + var body struct { + UserID string `json:"userId" binding:"required"` + Status string `json:"status" binding:"required"` + } + if err := ctx.ShouldBindJSON(r, &body); err != nil { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + // 检查是否存在 + user := s.sysUserService.SelectUserById(body.UserID) + if user.Id != body.UserID { + ctx.JSON(w, 200, result.ErrMsg("没有权限访问用户数据!")) + return + } + + // 与旧值相等不变更 + if user.Status == body.Status { + ctx.JSON(w, 200, result.ErrMsg("变更状态与旧值相等!")) + return + } + + SysUserApi := sysUserModel.SysUser{ + Id: body.UserID, + Status: body.Status, + } + rows := s.sysUserService.UpdateUser(SysUserApi) + if rows > 0 { + ctx.JSON(w, 200, result.Ok(nil)) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} diff --git a/features/sys_user/model/sys_user.go b/features/sys_user/model/sys_user.go new file mode 100644 index 0000000..b7a0259 --- /dev/null +++ b/features/sys_user/model/sys_user.go @@ -0,0 +1,42 @@ +package model + +import "ems.agt/features/sys_role/model" + +type SysUser struct { + Id string `json:"id" xorm:"pk 'id' autoincr"` + AccountId string `json:"accountId" xorm:"account_id"` + Name string `json:"name" xorm:"name"` + Sn string `json:"sn"` + Gender string `json:"gender"` + Description string `json:"description"` + TelephoneNumber string `json:"telephoneNumber" xorm:"telephone_number"` + Mobile string `json:"mobile"` + Email string `json:"email" xorm:"email"` + StartTime string `json:"startTime" xorm:"start_time"` + EndTime string `json:"endTime" xorm:"end_time"` + IdCardNumber string `json:"idCardNumber"` + EmployeeNumber string `json:"employeeNumber"` + Organize string `json:"organize"` + EmployeeType string `json:"employeeType"` + SupporterCorpName string `json:"supporterCorpName"` + RealName string `json:"realName" xorm:"real_name"` + Password string `json:"password" xorm:"-"` + PasswordSha512 string `json:"-" xorm:"-"` + ChangePasswordFlag int `json:"changePasswordFlag"` + PasswordExpiration string `json:"passwordExpiration" xorm:"password_expiration"` + Status string `json:"status"` + UserExpiration string `json:"userExpiration" xorm:"user_expiration"` + GroupName string `json:"groupName" xorm:"group_name"` + Profile string `json:"-" xorm:"profile"` + Phone string `json:"phone" xorm:"phone"` + CreateTime string `json:"createTime" xorm:"create_time"` + UpdateTime string `json:"updateTime" xorm:"update_time"` + Unit string `json:"unit" xorm:"unit"` + + // 角色对象组 + Roles []model.SysRole `json:"roles"` + // 角色ID - 参数提交绑定 + RoleID string `json:"roleId,omitempty"` + // 角色组 - 参数提交绑定 + RoleIDs []string `json:"roleIds,omitempty"` +} diff --git a/features/sys_user/service/repo_sys_user.go b/features/sys_user/service/repo_sys_user.go new file mode 100644 index 0000000..622b7e6 --- /dev/null +++ b/features/sys_user/service/repo_sys_user.go @@ -0,0 +1,578 @@ +package service + +import ( + "fmt" + "strings" + "time" + + sysRoleModel "ems.agt/features/sys_role/model" + sysUserModel "ems.agt/features/sys_user/model" + "ems.agt/lib/core/datasource" + "ems.agt/lib/core/utils/crypto" + "ems.agt/lib/core/utils/date" + "ems.agt/lib/core/utils/parse" + "ems.agt/lib/log" +) + +// 实例化数据层 RepoSysUser 结构体 +var NewRepoSysUser = &RepoSysUser{ + selectSql: `select u.id, + u.account_id, u.name, u.sn, u.gender, u.description, u.telephone_number, u.mobile, u.email, + u.start_time, u.end_time, u.id_card_number, u.employee_number, + u.organize, u.employee_type, u.supporter_corp_name, u.real_name, u.password, u.password_sha512, + u.change_password_flag,u.password_expiration, u.status, u.user_expiration, u.group_name, + u.profile, u.phone, u.create_time, u.update_time, u.unit, + r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status + from user u + left join sys_user_role ur on u.id = ur.user_id + left join sys_role r on r.role_id = ur.role_id`, + + sysUserMap: map[string]string{ + "id": "Id", + "account_id": "AccountId", + "name": "Name", + "sn": "Sn", + "gender": "Gender", + "description": "Description", + "telephone_number": "TelephoneNumber", + "mobile": "Mobile", + "email": "Email", + "start_time": "StartTime", + "end_time": "EndTime", + "id_card_number": "IdCardNumber", + "employee_number": "EmployeeNumber", + "organize": "Organize", + "employee_type": "EmployeeType", + "supporter_corp_name": "SupporterCorpName", + "real_name": "RealName", + "password": "Password", + "password_sha512": "PasswordSha512", + "change_password_flag": "ChangePasswordFlag", + "password_expiration": "PasswordExpiration", + "status": "Status", + "user_expiration": "UserExpiration", + "group_name": "GroupName", + "profile": "Profile", + "phone": "Phone", + "create_time": "CreateTime", + "update_time": "UpdateTime", + "unit": "Unit", + }, + + sysRoleMap: map[string]string{ + "role_id": "RoleID", + "role_name": "RoleName", + "role_key": "RoleKey", + "role_sort": "RoleSort", + "data_scope": "DataScope", + "role_status": "Status", + }, +} + +// RepoSysUser 用户表 数据层处理 +type RepoSysUser struct { + // 查询视图对象SQL + selectSql string + // 用户信息实体映射 + sysUserMap map[string]string + // 用户角色实体映射 一对多 + sysRoleMap map[string]string +} + +// convertResultRows 将结果记录转实体结果组 +func (r *RepoSysUser) convertResultRows(rows []map[string]any) []sysUserModel.SysUser { + arr := make([]sysUserModel.SysUser, 0) + + for _, row := range rows { + sysUser := sysUserModel.SysUser{} + sysRole := sysRoleModel.SysRole{} + sysUser.Roles = []sysRoleModel.SysRole{} + + for key, value := range row { + if keyMapper, ok := r.sysUserMap[key]; ok { + datasource.SetFieldValue(&sysUser, keyMapper, value) + } + if keyMapper, ok := r.sysRoleMap[key]; ok { + datasource.SetFieldValue(&sysRole, keyMapper, value) + } + } + + if sysRole.RoleKey != "" { + sysUser.Roles = append(sysUser.Roles, sysRole) + } + + one := true + for i, a := range arr { + if a.Id == sysUser.Id { + arrUser := &arr[i] + arrUser.Roles = append(arrUser.Roles, sysUser.Roles...) + one = false + break + } + } + if one { + arr = append(arr, sysUser) + } + } + + return arr +} + +// SelectUserPage 根据条件分页查询用户列表 +func (r *RepoSysUser) SelectUserPage(query map[string]any) map[string]any { + selectUserSql := `select u.id, + u.account_id, u.name, u.sn, u.gender, u.description, u.telephone_number, u.mobile, u.email, + u.start_time, u.end_time, u.id_card_number, u.employee_number, + u.organize, u.employee_type, u.supporter_corp_name, u.real_name, + u.change_password_flag,u.password_expiration, u.status, u.user_expiration, u.group_name, + u.profile, u.phone, u.create_time, u.update_time, u.unit + from user u` + selectUserTotalSql := `select count(distinct u.id) as 'total' from user u` + + // 查询条件拼接 + var conditions []string + var params []any + if v, ok := query["accountId"]; ok && v != "" { + conditions = append(conditions, "u.account_id = ?") + params = append(params, v) + } + if v, ok := query["name"]; ok && v != "" { + conditions = append(conditions, "u.name concat('%', concat(?, '%'))") + params = append(params, v) + } + if v, ok := query["status"]; ok && v != "" { + conditions = append(conditions, "u.status = ?") + params = append(params, v) + } + if v, ok := query["phonenumber"]; ok && v != "" { + conditions = append(conditions, "u.phonenumber like concat('%', concat(?, '%'))") + params = append(params, v) + } + beginTime, ok := query["beginTime"] + if !ok { + beginTime, ok = query["params[beginTime]"] + } + if ok && beginTime != "" { + conditions = append(conditions, "u.login_date >= ?") + beginDate := date.ParseStrToDate(beginTime.(string), date.YYYY_MM_DD) + params = append(params, beginDate.UnixMilli()) + } + endTime, ok := query["endTime"] + if !ok { + endTime, ok = query["params[endTime]"] + } + if ok && endTime != "" { + conditions = append(conditions, "u.login_date <= ?") + endDate := date.ParseStrToDate(endTime.(string), date.YYYY_MM_DD) + params = append(params, endDate.UnixMilli()) + } + if v, ok := query["deptId"]; ok && v != "" { + conditions = append(conditions, "(u.dept_id = ? or u.dept_id in ( select t.dept_id from sys_dept t where find_in_set(?, ancestors) ))") + params = append(params, v) + params = append(params, v) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询数量 长度为0直接返回 + totalSql := selectUserTotalSql + whereSql + totalRows, err := datasource.RawDB("", totalSql, params) + if err != nil { + log.Errorf("total err => %v", err) + } + total := parse.Number(totalRows[0]["total"]) + if total == 0 { + return map[string]any{ + "total": total, + "rows": []sysUserModel.SysUser{}, + } + } + + // 分页 + pageNum, pageSize := datasource.PageNumSize(query["pageNum"], query["pageSize"]) + pageSql := " limit ?,? " + params = append(params, pageNum*pageSize) + params = append(params, pageSize) + + // 查询数据 + querySql := selectUserSql + whereSql + pageSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + log.Errorf("query err => %v", err) + } + + // 转换实体 + rows := r.convertResultRows(results) + return map[string]any{ + "total": total, + "rows": rows, + } +} + +// SelectAllocatedPage 根据条件分页查询分配用户角色列表 +func (r *RepoSysUser) SelectAllocatedPage(query map[string]any) map[string]any { + // 查询条件拼接 + var conditions []string + var params []any + if v, ok := query["name"]; ok && v != "" { + conditions = append(conditions, "u.name like concat('%', concat(?, '%'))") + params = append(params, v) + } + if v, ok := query["phone"]; ok && v != "" { + conditions = append(conditions, "u.phone like concat('%', concat(?, '%'))") + params = append(params, v) + } + if v, ok := query["status"]; ok && v != "" { + conditions = append(conditions, "u.status = ?") + params = append(params, v) + } + // 分配角色用户 + if allocated, ok := query["allocated"]; ok && allocated != "" { + if roleId, ok := query["roleId"]; ok && roleId != "" { + if parse.Boolean(allocated) { + conditions = append(conditions, "r.role_id = ?") + params = append(params, roleId) + } else { + conditions = append(conditions, `(r.role_id != ? or r.role_id IS NULL) + and u.id not in ( + select u.id from user u + inner join sys_user_role ur on u.id = ur.user_id + and ur.role_id = ? + )`) + params = append(params, roleId) + params = append(params, roleId) + } + + } + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询数量 长度为0直接返回 + totalSql := `select count(distinct u.id) as 'total' from user u + left join sys_user_role ur on u.id = ur.user_id + left join sys_role r on r.role_id = ur.role_id` + totalRows, err := datasource.RawDB("", totalSql+whereSql, params) + if err != nil { + log.Errorf("total err => %v", err) + } + total := parse.Number(totalRows[0]["total"]) + if total == 0 { + return map[string]any{ + "total": total, + "rows": []sysUserModel.SysUser{}, + } + } + + // 分页 + pageNum, pageSize := datasource.PageNumSize(query["pageNum"], query["pageSize"]) + pageSql := " limit ?,? " + params = append(params, pageNum*pageSize) + params = append(params, pageSize) + + // 查询数据 + querySql := `select distinct + u.id, u.account_id, u.name, u.gender, u.email, + u.phone, u.status, u.create_time, u.real_name + from user u + left join sys_user_role ur on u.id = ur.user_id + left join sys_role r on r.role_id = ur.role_id` + querySql = querySql + whereSql + pageSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + log.Errorf("query err => %v", err) + } + + // 转换实体 + rows := r.convertResultRows(results) + return map[string]any{ + "total": total, + "rows": rows, + } +} + +// SelectUserList 根据条件查询用户列表 +func (r *RepoSysUser) SelectUserList(sysUser sysUserModel.SysUser, dataScopeSQL string) []sysUserModel.SysUser { + selectUserSql := `select + u.id, u.account_id, u.name, u.real_name, u.email, u.gender, u.phone, u.create_time, u.status, u.description + from user u` + + // 查询条件拼接 + var conditions []string + var params []any + if sysUser.AccountId != "" { + conditions = append(conditions, "u.account_id = ?") + params = append(params, sysUser.AccountId) + } + if sysUser.Name != "" { + conditions = append(conditions, "u.name like concat('%', concat(?, '%'))") + params = append(params, sysUser.Name) + } + if sysUser.Status != "" { + conditions = append(conditions, "u.status = ?") + params = append(params, sysUser.Status) + } + if sysUser.Phone != "" { + conditions = append(conditions, "u.phone like concat('%', concat(?, '%'))") + params = append(params, sysUser.Phone) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询数据 + querySql := selectUserSql + whereSql + dataScopeSQL + rows, err := datasource.RawDB("", querySql, params) + if err != nil { + log.Errorf("query err => %v", err) + return []sysUserModel.SysUser{} + } + return r.convertResultRows(rows) +} + +// SelectUserByIds 通过用户ID查询用户 +func (r *RepoSysUser) SelectUserByIds(userIds []string) []sysUserModel.SysUser { + placeholder := datasource.KeyPlaceholderByQuery(len(userIds)) + querySql := r.selectSql + " where u.id in (" + placeholder + ")" + parameters := datasource.ConvertIdsSlice(userIds) + results, err := datasource.RawDB("", querySql, parameters) + if err != nil { + log.Errorf("query err => %v", err) + return []sysUserModel.SysUser{} + } + // 转换实体 + return r.convertResultRows(results) +} + +// SelectUserByUserName 通过用户登录账号查询用户 +func (r *RepoSysUser) SelectUserByUserName(userName string) sysUserModel.SysUser { + querySql := r.selectSql + " where u.name = ?" + results, err := datasource.RawDB("", querySql, []any{userName}) + if err != nil { + log.Errorf("query err => %v", err) + return sysUserModel.SysUser{} + } + // 转换实体 + rows := r.convertResultRows(results) + if len(rows) > 0 { + return rows[0] + } + return sysUserModel.SysUser{} +} + +// InsertUser 新增用户信息 +func (r *RepoSysUser) InsertUser(sysUser sysUserModel.SysUser) string { + // 参数拼接 + params := make(map[string]any) + if sysUser.AccountId != "" { + params["account_id"] = sysUser.AccountId + } + if sysUser.Name != "" { + params["name"] = sysUser.Name + } + if sysUser.Sn != "" { + params["sn"] = sysUser.Sn + } else { + params["sn"] = "" + } + if sysUser.RealName != "" { + params["real_name"] = sysUser.RealName + } else { + params["real_name"] = "" + } + if sysUser.Gender != "" { + params["gender"] = sysUser.Gender + } + if sysUser.Email != "" { + params["email"] = sysUser.Email + } else { + params["email"] = "" + } + if sysUser.Phone != "" { + params["phone"] = sysUser.Phone + } else { + params["phone"] = "" + } + if sysUser.Unit != "" { + params["unit"] = sysUser.Unit + } else { + params["unit"] = "" + } + + if sysUser.Organize != "" { + params["organize"] = sysUser.Organize + } + if sysUser.Password != "" { + password := crypto.BcryptHash(sysUser.Password) + params["password"] = password + } + if sysUser.Status != "" { + params["status"] = sysUser.Status + } + if sysUser.PasswordExpiration != "" { + params["password_expiration"] = sysUser.PasswordExpiration + } + if sysUser.UserExpiration != "" { + params["user_expiration"] = sysUser.UserExpiration + } + if sysUser.GroupName != "" { + params["group_name"] = sysUser.GroupName + } + params["create_time"] = time.Now() + + // 构建执行语句 + keys, placeholder, values := datasource.KeyPlaceholderValueByInsert(params) + sql := "insert into user (" + strings.Join(keys, ",") + ")values(" + placeholder + ")" + + // 执行插入 + results, err := datasource.ExecDB("", sql, values) + if err != nil { + log.Errorf("insert row : %v", err.Error()) + return "" + } + insertId, err := results.LastInsertId() + if err != nil { + log.Errorf("insert row : %v", err.Error()) + return "" + } + return fmt.Sprint(insertId) +} + +// UpdateUser 修改用户信息 +func (r *RepoSysUser) UpdateUser(sysUser sysUserModel.SysUser) int64 { + // 参数拼接 + params := make(map[string]any) + if sysUser.Name != "" { + params["name"] = sysUser.Name + } + if sysUser.Sn != "" { + params["sn"] = sysUser.Sn + } else { + params["sn"] = "" + } + if sysUser.RealName != "" { + params["real_name"] = sysUser.RealName + } else { + params["real_name"] = "" + } + if sysUser.Gender != "" { + params["gender"] = sysUser.Gender + } + if sysUser.Email != "" { + params["email"] = sysUser.Email + } else { + params["email"] = "" + } + if sysUser.Phone != "" { + params["phone"] = sysUser.Phone + } else { + params["phone"] = "" + } + if sysUser.Unit != "" { + params["unit"] = sysUser.Unit + } else { + params["unit"] = "" + } + + if sysUser.Organize != "" { + params["organize"] = sysUser.Organize + } + if sysUser.Password != "" { + password := crypto.BcryptHash(sysUser.Password) + params["password"] = password + } + if sysUser.Status != "" { + params["status"] = sysUser.Status + } + if sysUser.PasswordExpiration != "" { + params["password_expiration"] = sysUser.PasswordExpiration + } + if sysUser.UserExpiration != "" { + params["user_expiration"] = sysUser.UserExpiration + } + if sysUser.GroupName != "" { + params["group_name"] = sysUser.GroupName + } + params["update_time"] = time.Now() + + // 构建执行语句 + keys, values := datasource.KeyValueByUpdate(params) + sql := "update user set " + strings.Join(keys, ",") + " where id = ?" + + // 执行更新 + values = append(values, sysUser.Id) + results, err := datasource.ExecDB("", sql, values) + if err != nil { + log.Errorf("update row : %v", err.Error()) + return 0 + } + affected, err := results.RowsAffected() + if err != nil { + log.Errorf("update err => %v", err) + return 0 + } + return affected +} + +// DeleteUserByIds 批量删除用户信息 +func (r *RepoSysUser) DeleteUserByIds(userIds []string) int64 { + placeholder := datasource.KeyPlaceholderByQuery(len(userIds)) + sql := "delete from user where id in (" + placeholder + ")" + parameters := datasource.ConvertIdsSlice(userIds) + results, err := datasource.ExecDB("", sql, parameters) + if err != nil { + log.Errorf("delete err => %v", err) + return 0 + } + affected, err := results.RowsAffected() + if err != nil { + log.Errorf("delete err => %v", err) + return 0 + } + return affected +} + +// CheckUniqueUser 校验用户信息是否唯一 +func (r *RepoSysUser) CheckUniqueUser(sysUser sysUserModel.SysUser) string { + // 查询条件拼接 + var conditions []string + var params []any + if sysUser.Name != "" { + conditions = append(conditions, "name = ?") + params = append(params, sysUser.Name) + } + if sysUser.AccountId != "" { + conditions = append(conditions, "account_id = ?") + params = append(params, sysUser.AccountId) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } else { + return "" + } + + // 查询数据 + querySql := "select id as 'str' from user " + whereSql + " limit 1" + results, err := datasource.RawDB("", querySql, params) + if err != nil { + log.Errorf("query err %v", err) + } + if len(results) > 0 { + return fmt.Sprintf("%v", results[0]["str"]) + } + return "" +} diff --git a/features/sys_user/service/service_sys_user.go b/features/sys_user/service/service_sys_user.go new file mode 100644 index 0000000..0401d30 --- /dev/null +++ b/features/sys_user/service/service_sys_user.go @@ -0,0 +1,150 @@ +package service + +import ( + "errors" + "fmt" + + sysUserModel "ems.agt/features/sys_user/model" + sysuserrole "ems.agt/features/sys_user_role" +) + +// 实例化服务层 ServiceSysUser 结构体 +var NewServiceSysUser = &ServiceSysUser{ + sysUserRepository: NewRepoSysUser, + sysUserRoleRepository: sysuserrole.NewRepoSysUserRole, +} + +// ServiceSysUser 用户 服务层处理 +type ServiceSysUser struct { + // 用户服务 + sysUserRepository *RepoSysUser + // 用户与角色服务 + sysUserRoleRepository *sysuserrole.RepoSysUserRole +} + +// SelectUserPage 根据条件分页查询用户列表 +func (r *ServiceSysUser) SelectUserPage(query map[string]any) map[string]any { + return r.sysUserRepository.SelectUserPage(query) +} + +// SelectUserList 根据条件查询用户列表 +func (r *ServiceSysUser) SelectUserList(sysUser sysUserModel.SysUser) []sysUserModel.SysUser { + return []sysUserModel.SysUser{} +} + +// SelectAllocatedPage 根据条件分页查询分配用户角色列表 +func (r *ServiceSysUser) SelectAllocatedPage(query map[string]any) map[string]any { + return r.sysUserRepository.SelectAllocatedPage(query) +} + +// SelectUserByUserName 通过用户名查询用户 +func (r *ServiceSysUser) SelectUserByUserName(userName string) sysUserModel.SysUser { + return r.sysUserRepository.SelectUserByUserName(userName) +} + +// SelectUserById 通过用户ID查询用户 +func (r *ServiceSysUser) SelectUserById(userId string) sysUserModel.SysUser { + if userId == "" { + return sysUserModel.SysUser{} + } + users := r.sysUserRepository.SelectUserByIds([]string{userId}) + if len(users) > 0 { + return users[0] + } + return sysUserModel.SysUser{} +} + +// InsertUser 新增用户信息 +func (r *ServiceSysUser) InsertUser(sysUser sysUserModel.SysUser) string { + // 新增用户信息 + insertId := r.sysUserRepository.InsertUser(sysUser) + if insertId != "" { + // 新增用户角色信息 + r.insertUserRole(insertId, sysUser.RoleIDs) + } + return insertId +} + +// insertUserRole 新增用户角色信息 +func (r *ServiceSysUser) insertUserRole(userId string, roleIds []string) int64 { + if userId == "" || len(roleIds) <= 0 { + return 0 + } + + sysUserRoles := []sysuserrole.SysUserRole{} + for _, roleId := range roleIds { + // 管理员角色禁止操作,只能通过配置指定用户ID分配 + if roleId == "" || roleId == "1" { + continue + } + sysUserRoles = append(sysUserRoles, sysuserrole.NewSysUserRole(userId, roleId)) + } + + return r.sysUserRoleRepository.BatchUserRole(sysUserRoles) +} + +// UpdateUser 修改用户信息 +func (r *ServiceSysUser) UpdateUser(sysUser sysUserModel.SysUser) int64 { + return r.sysUserRepository.UpdateUser(sysUser) +} + +// UpdateUserAndRolePost 修改用户信息同时更新角色和岗位 +func (r *ServiceSysUser) UpdateUserAndRolePost(sysUser sysUserModel.SysUser) int64 { + userId := fmt.Sprint(sysUser.Id) + // 删除用户与角色关联 + r.sysUserRoleRepository.DeleteUserRole([]string{userId}) + // 新增用户角色信息 + r.insertUserRole(userId, sysUser.RoleIDs) + return r.sysUserRepository.UpdateUser(sysUser) +} + +// DeleteUserByIds 批量删除用户信息 +func (r *ServiceSysUser) DeleteUserByIds(userIds []string) (int64, error) { + // 检查是否存在 + users := r.sysUserRepository.SelectUserByIds(userIds) + if len(users) <= 0 { + return 0, errors.New("没有权限访问用户数据!") + } + if len(users) == len(userIds) { + // 删除用户与角色关联 + r.sysUserRoleRepository.DeleteUserRole(userIds) + // ... 注意其他userId进行关联的表 + // 删除用户 + rows := r.sysUserRepository.DeleteUserByIds(userIds) + return rows, nil + } + return 0, errors.New("删除用户信息失败!") +} + +// CheckUniqueUserName 校验用户名称是否唯一 +func (r *ServiceSysUser) CheckUniqueUserName(accountId, userId string) bool { + uniqueId := r.sysUserRepository.CheckUniqueUser(sysUserModel.SysUser{ + AccountId: accountId, + }) + if uniqueId == userId { + return true + } + return uniqueId == "" +} + +// CheckUniquePhone 校验手机号码是否唯一 +func (r *ServiceSysUser) CheckUniquePhone(phonenumber, userId string) bool { + uniqueId := r.sysUserRepository.CheckUniqueUser(sysUserModel.SysUser{ + Phone: phonenumber, + }) + if uniqueId == userId { + return true + } + return uniqueId == "" +} + +// CheckUniqueEmail 校验email是否唯一 +func (r *ServiceSysUser) CheckUniqueEmail(email, userId string) bool { + uniqueId := r.sysUserRepository.CheckUniqueUser(sysUserModel.SysUser{ + Email: email, + }) + if uniqueId == userId { + return true + } + return uniqueId == "" +} diff --git a/features/sys_user_role/model_sys_user_role.go b/features/sys_user_role/model_sys_user_role.go new file mode 100644 index 0000000..bc493eb --- /dev/null +++ b/features/sys_user_role/model_sys_user_role.go @@ -0,0 +1,15 @@ +package sysuserrole + +// SysUserRole 用户和角色关联对象 sys_user_role +type SysUserRole struct { + UserID string `json:"userId"` // 用户ID + RoleID string `json:"roleId"` // 角色ID +} + +// NewSysUserRole 创建用户和角色关联对象的构造函数 +func NewSysUserRole(userID string, roleID string) SysUserRole { + return SysUserRole{ + UserID: userID, + RoleID: roleID, + } +} diff --git a/features/sys_user_role/repo_sys_user_role.go b/features/sys_user_role/repo_sys_user_role.go new file mode 100644 index 0000000..97585a1 --- /dev/null +++ b/features/sys_user_role/repo_sys_user_role.go @@ -0,0 +1,87 @@ +package sysuserrole + +import ( + "fmt" + "strings" + + "ems.agt/lib/core/datasource" + "ems.agt/lib/core/utils/parse" + "ems.agt/lib/log" +) + +// 实例化数据层 RepoSysUserRole 结构体 +var NewRepoSysUserRole = &RepoSysUserRole{} + +// RepoSysUserRole 用户与角色关联表 数据层处理 +type RepoSysUserRole struct{} + +// CountUserRoleByRoleId 通过角色ID查询角色使用数量 +func (r *RepoSysUserRole) CountUserRoleByRoleId(roleId string) int64 { + querySql := "select count(1) as total from sys_user_role where role_id = ?" + results, err := datasource.RawDB("", querySql, []any{roleId}) + if err != nil { + log.Errorf("query err => %v", err) + return 0 + } + if len(results) > 0 { + return parse.Number(results[0]["total"]) + } + return 0 +} + +// BatchUserRole 批量新增用户角色信息 +func (r *RepoSysUserRole) BatchUserRole(sysUserRoles []SysUserRole) int64 { + keyValues := make([]string, 0) + for _, item := range sysUserRoles { + keyValues = append(keyValues, fmt.Sprintf("(%s,%s)", item.UserID, item.RoleID)) + } + sql := "insert into sys_user_role(user_id, role_id) values " + strings.Join(keyValues, ",") + results, err := datasource.ExecDB("", sql, nil) + if err != nil { + log.Errorf("delete err => %v", err) + return 0 + } + affected, err := results.RowsAffected() + if err != nil { + log.Errorf("delete err => %v", err) + return 0 + } + return affected +} + +// DeleteUserRole 批量删除用户和角色关联 +func (r *RepoSysUserRole) DeleteUserRole(userIds []string) int64 { + placeholder := datasource.KeyPlaceholderByQuery(len(userIds)) + sql := "delete from sys_user_role where user_id in (" + placeholder + ")" + parameters := datasource.ConvertIdsSlice(userIds) + results, err := datasource.ExecDB("", sql, parameters) + if err != nil { + log.Errorf("delete err => %v", err) + return 0 + } + affected, err := results.RowsAffected() + if err != nil { + log.Errorf("delete err => %v", err) + return 0 + } + return affected +} + +// DeleteUserRoleByRoleId 批量取消授权用户角色 +func (r *RepoSysUserRole) DeleteUserRoleByRoleId(roleId string, userIds []string) int64 { + placeholder := datasource.KeyPlaceholderByQuery(len(userIds)) + sql := "delete from sys_user_role where role_id= ? and user_id in (" + placeholder + ")" + parameters := datasource.ConvertIdsSlice(userIds) + parameters = append([]any{roleId}, parameters...) + results, err := datasource.ExecDB("", sql, parameters) + if err != nil { + log.Errorf("delete err => %v", err) + return 0 + } + affected, err := results.RowsAffected() + if err != nil { + log.Errorf("delete err => %v", err) + return 0 + } + return affected +} diff --git a/features/trace/tcpdump.go b/features/trace/tcpdump.go new file mode 100644 index 0000000..e3d4c4f --- /dev/null +++ b/features/trace/tcpdump.go @@ -0,0 +1,272 @@ +package trace + +import ( + "fmt" + "net" + "net/http" + "strings" + "time" + + "ems.agt/lib/core/cmd" + "ems.agt/lib/core/conf" + "ems.agt/lib/core/file" + "ems.agt/lib/core/utils/ctx" + "ems.agt/lib/core/vo/result" + "ems.agt/lib/dborm" + "ems.agt/lib/log" + "ems.agt/restagent/config" +) + +var ( + UriTcpdumpTask = config.DefaultUriPrefix + "/traceManagement/{apiVersion}/tcpdumpNeTask" + CustomUriTcpdumpTask = config.UriPrefix + "/traceManagement/{apiVersion}/tcpdumpNeTask" // decode message api + + UriTcpdumpPcapDownload = config.DefaultUriPrefix + "/traceManagement/{apiVersion}/tcpdumpPcapDownload" + CustomUriTcpdumpPcapDownload = config.UriPrefix + "/traceManagement/{apiVersion}/tcpdumpPcapDownload" // decode message api + + UriTcpdumpNeUPFTask = config.DefaultUriPrefix + "/traceManagement/{apiVersion}/tcpdumpNeUPFTask" + CustomUriTcpdumpNeUPFTask = config.UriPrefix + "/traceManagement/{apiVersion}/tcpdumpNeUPFTask" // decode message api +) + +// NeInfo 网元信息 +func NeInfo(neType, neId string) (*dborm.NeInfo, error) { + neInfo, err := dborm.XormGetNeInfo(neType, neId) + if err != nil { + log.Error("dborm.XormGetNeInfo is failed:", err) + return nil, err + } + if neInfo == nil || neInfo.Ip == "" { + return nil, fmt.Errorf("not ne_info or not IP") + } + return neInfo, nil +} + +// TcpdumpNeTask 网元发送执行 pcap +func TcpdumpNeTask(w http.ResponseWriter, r *http.Request) { + var body struct { + NeType string `json:"neType"` // 网元类型 + NeId string `json:"neId"` // 网元ID + Timeout int `json:"timeout"` // 超时时间 + Cmd string `json:"cmd"` // 命令 + } + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.NeType == "" || body.NeId == "" || body.Timeout < 5 || body.Cmd == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + neInfo, err := NeInfo(body.NeType, body.NeId) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + filePcapName := fmt.Sprintf("tmp_%s_%s_%d.pcap", body.NeType, body.NeId, time.Now().UnixMilli()) + fileLogName := fmt.Sprintf("tmp_%s_%s_%d.log", body.NeType, body.NeId, time.Now().UnixMilli()) + writeLog := fmt.Sprintf(" >> %s 2>&1 \ncat %s", fileLogName, fileLogName) // 执行信息写入日志文件,放置弹出code 127 + cmdStr := fmt.Sprintf("cd /tmp \ntimeout %d tcpdump -i any %s -s0 -w %s", body.Timeout, body.Cmd, filePcapName) + usernameNe := conf.Get("ne.user").(string) // 网元统一用户 + sshHost := fmt.Sprintf("%s@%s", usernameNe, neInfo.Ip) + msg, err := cmd.ExecWithCheck("ssh", sshHost, cmdStr+writeLog) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + ctx.JSON(w, 200, result.OkData(map[string]any{ + "cmd": cmdStr, + "msg": msg, + "fileName": filePcapName, + })) +} + +// TcpdumpPcapDownload 网元抓包pcap文件下载 +func TcpdumpPcapDownload(w http.ResponseWriter, r *http.Request) { + var body struct { + NeType string `json:"neType"` // 网元类型 + NeId string `json:"neId"` // 网元ID + FileName string `json:"fileName"` // 文件名 + } + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.NeType == "" || body.NeId == "" || body.FileName == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + neInfo, err := NeInfo(body.NeType, body.NeId) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + nePath := fmt.Sprintf("/tmp/%s", body.FileName) + localPath := fmt.Sprintf("%s/tcpdump/pcap/%s", conf.Get("ne.omcdir"), body.FileName) + err = file.FileSCPNeToLocal(neInfo.Ip, nePath, localPath) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + ctx.FileAttachment(w, r, localPath, body.FileName) +} + +// TcpdumpNeUPFTask 网元UPF发送执行 pcap +func TcpdumpNeUPFTask(w http.ResponseWriter, r *http.Request) { + var body struct { + NeType string `json:"neType"` // 网元类型 + NeId string `json:"neId"` // 网元ID + Cmd string `json:"cmd"` // 命令 + RunType string `json:"runType"` // 执行开始start还是停止stop + } + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.NeType != "UPF" || body.NeId == "" || body.Cmd == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + neInfo, err := NeInfo(body.NeType, body.NeId) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + // 开始 + if body.RunType == "start" { + // 创建TCP连接 + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", neInfo.Ip, 5002)) + if err != nil { + conn.Close() + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + filePcapName := fmt.Sprintf("tmp_%s_%s.pcap", body.NeType, body.NeId) + cmdStr := fmt.Sprintf("pcap dispatch trace on max 100000 file %s", filePcapName) + + fmt.Fprintln(conn, cmdStr) + + // 读取内容 + time.Sleep(time.Duration(200) * time.Millisecond) + buf := make([]byte, 1024*8) + n, err := conn.Read(buf) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + } else { + str := string(buf[0:n]) + s := strings.Index(str, "pcap dispatch trace:") + if s != -1 { + e := strings.Index(str, "\r\nupfd1#") + str = str[s:e] + } else { + str = fmt.Sprintf("Executed, please stop before proceeding %d", n) + } + ctx.JSON(w, 200, result.OkData(map[string]any{ + "cmd": cmdStr, + "msg": str, + "fileName": filePcapName, + })) + } + conn.Close() + return + } + // 停止 + if body.RunType == "stop" { + // 创建TCP连接 + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", neInfo.Ip, 5002)) + if err != nil { + conn.Close() + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + filePcapName := fmt.Sprintf("tmp_%s_%s.pcap", body.NeType, body.NeId) + cmdStr := "pcap dispatch trace off" + + fmt.Fprintln(conn, cmdStr) + + // 读取内容 + time.Sleep(time.Duration(200) * time.Millisecond) + buf := make([]byte, 1024*8) + n, err := conn.Read(buf) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + } else { + str := string(buf[0:n]) + s := strings.Index(str, "pcap dispatch trace:") + if s == -1 { + s = strings.Index(str, "Write ") + } + if s != -1 { + e := strings.Index(str, "\r\nupfd1#") + str = str[s:e] + } else { + str = "No stoppable found" + } + + ctx.JSON(w, 200, result.OkData(map[string]any{ + "cmd": cmdStr, + "msg": str, + "fileName": filePcapName, + })) + } + conn.Close() + return + } + + // 开始 -脚本 + if body.RunType == "start2" { + fileLogName := fmt.Sprintf("tmp_%s_%s.log", body.NeType, body.NeId) + filePcapName := fmt.Sprintf("tmp_%s_%s.pcap", body.NeType, body.NeId) + // 复制文件到网元上 + err := file.FileSCPLocalToNe(neInfo.Ip, "C:\\AMP\\Probject\\ems_backend\\restagent\\backup\\upf_pcap", "/tmp") + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + writeLog := fmt.Sprintf(" >> %s 2>&1 \ncat %s", fileLogName, fileLogName) // 执行信息写入日志文件,放置弹出code 127 + cmdStr := fmt.Sprintf("cd /tmp \nchmod +x upf_pcap\n./upf_pcap '192.168.4.139' 'root' 'Admin123@pl' 'pcap dispatch trace on max 100000 file %s' %s ", fileLogName, writeLog) + + usernameNe := conf.Get("ne.user").(string) // 网元统一用户 + sshHost := fmt.Sprintf("%s@%s", usernameNe, neInfo.Ip) + msg, err := cmd.ExecWithCheck("ssh", sshHost, cmdStr) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + } else { + ctx.JSON(w, 200, result.OkData(map[string]any{ + "cmd": cmdStr, + "msg": msg, + "fileName": filePcapName, + })) + } + return + } + // 停止 -脚本 + if body.RunType == "stop2" { + fileLogName := fmt.Sprintf("tmp_%s_%s.log", body.NeType, body.NeId) + filePcapName := fmt.Sprintf("tmp_%s_%s.pcap", body.NeType, body.NeId) + // cmdStr := "cd /tmp \nexpect /tmp/cat.sh " + err := file.FileSCPLocalToNe(neInfo.Ip, "C:\\AMP\\Probject\\ems_backend\\restagent\\backup\\upf_pcap", "/tmp") + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + writeLog := fmt.Sprintf(" >> %s 2>&1 \ncat %s", fileLogName, fileLogName) // 执行信息写入日志文件,放置弹出code 127 + cmdStr := fmt.Sprintf("cd /tmp \nchmod +x upf_pcap\n./upf_pcap '192.168.4.139' 'root' 'Admin123@pl' 'pcap dispatch trace off' %s ", writeLog) + + usernameNe := conf.Get("ne.user").(string) // 网元统一用户 + sshHost := fmt.Sprintf("%s@%s", usernameNe, neInfo.Ip) + msg, err := cmd.ExecWithCheck("ssh", sshHost, cmdStr) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + } else { + ctx.JSON(w, 200, result.OkData(map[string]any{ + "cmd": cmdStr, + "msg": msg, + "fileName": filePcapName, + })) + } + return + } + + ctx.JSON(w, 200, result.ErrMsg("runType is start or stop")) +} diff --git a/features/trace/trace.go b/features/trace/trace.go new file mode 100644 index 0000000..b22382c --- /dev/null +++ b/features/trace/trace.go @@ -0,0 +1,437 @@ +package trace + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strconv" + "strings" + + "github.com/go-resty/resty/v2" + "github.com/gorilla/mux" + + "ems.agt/lib/dborm" + "ems.agt/lib/global" + "ems.agt/lib/log" + "ems.agt/lib/run" + "ems.agt/lib/services" + "ems.agt/restagent/config" +) + +var ( + UriTraceTaskV1 = config.DefaultUriPrefix + "/traceManagement/v1/subscriptions" + UriTraceTask = config.DefaultUriPrefix + "/traceManagement/{apiVersion}/subscriptions" + UriTraceRawMsg = config.DefaultUriPrefix + "/traceManagement/{apiVersion}/rawMessage/{id}" + UriTraceDecMsg = config.DefaultUriPrefix + "/traceManagement/{apiVersion}/decMessage/{id}" // decode message api + + CustomUriTraceTaskV1 = config.UriPrefix + "/traceManagement/v1/subscriptions" + CustomUriTraceTask = config.UriPrefix + "/traceManagement/{apiVersion}/subscriptions" +) + +type TraceTask struct { + Id int `json:"id" xorm:"pk 'id' autoincr"` + TraceType string `json:"traceType"` + StartTime string `json:"startTime"` + EndTime string `json:"endTime"` + Imsi string `json:"imsi"` + Msisdn string `json:"msisdn"` + SrcIp string `json:"srcIp"` + DstIp string `json:"dstIp"` + SignalPort int16 `json:"signalPort"` + NeType string `json:"neType"` + NeId string `json:"neId"` + UeIp string `json:"ueIp"` + Interfaces []string `json:"interfaces"` + NotifyUrl string `json:"notifyUrl" xorm:"-"` + Status string `json:"-" xorm:"status"` + SuccNEs []string `json:"-" xorm:"succ_nes"` + FailNEs []string `json:"-" xorm:"fail_nes"` + AccountID string `json:"accountId" xorm:"account_id"` + Comment string `json:"comment" xorm:"comment"` + UpdateTime string `json:"-" xorm:"-"` +} + +var client = resty.New() + +/* +func init() { + client.SetTimeout(3 * time.Second) +} +*/ + +// Post trace task to NF/NFs +func PostTraceTaskToNF(w http.ResponseWriter, r *http.Request) { + log.Debug("PostTraceTaskToNF processing... ") + + //vars := mux.Vars(r) + + token, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + log.Debug("AccessToken:", token) + + body, err := io.ReadAll(io.LimitReader(r.Body, int64(config.GetYamlConfig().Params.UriMaxLen))) + if err != nil { + log.Error("io.ReadAll is failed:", err) + services.ResponseNotFound404UriNotExist(w, r) + return + } + log.Trace("body:", string(body)) + + traceTask := new(TraceTask) + _ = json.Unmarshal(body, traceTask) + log.Debug("traceTask:", traceTask) + + var neTypes []string + // do not set device + if traceTask.NeType == "" { + // query neType by interface + if len(traceTask.Interfaces) > 0 { + err := dborm.XormGetSingleColStringArrayByIn("trace_info", "ne_type", "interface", traceTask.Interfaces, &neTypes) + if err != nil { + log.Error("Failed to dborm.XormGetSingleCol:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + } else { + neTypes = []string{"AMF", "SMF", "UDM", "AUSF", "UPF"} + } + } else { + neTypes = append(neTypes, traceTask.NeType) + } + log.Debug("neTypes:", neTypes) + + traceTask.Status = "Inactive" + _, err = dborm.XormInsertTableOne("trace_task", traceTask) + if err != nil { + log.Error("Failed to dborm.XormInsertTableOne:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + traceTask.NotifyUrl = config.GetYamlConfig().OMC.GtpUri + log.Trace("traceTask:", traceTask) + + for _, neType := range neTypes { + var neInfos []dborm.NeInfo + if traceTask.NeId == "" { + err := dborm.XormGetNeInfoByNeType(neType, &neInfos) + if err != nil { + log.Error("Failed to dborm.XormGetNeInfoByNeType:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + } else { + neInfo, err := dborm.XormGetNeInfo(neType, traceTask.NeId) + if err != nil { + log.Error("Failed to dborm.XormGetNeInfoByNeType:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + neInfos = append(neInfos, *neInfo) + } + for _, neInfo := range neInfos { + hostUri := fmt.Sprintf("http://%s:%v", neInfo.Ip, neInfo.Port) + requestURI2NF := fmt.Sprintf("%s%s", hostUri, UriTraceTaskV1) + log.Debug("requestURI2NF:", requestURI2NF) + + body, _ := json.Marshal(traceTask) + log.Debug("body:", string(body)) + resp, err := client.R(). + EnableTrace(). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + SetBody(body). + Post(requestURI2NF) + if err != nil { + log.Error("Failed to Post:", err) + failNE := fmt.Sprintf("%s.%s", neInfo.NeType, neInfo.NeId) + traceTask.FailNEs = append(traceTask.FailNEs, failNE) + } else { + switch resp.StatusCode() { + case http.StatusOK, http.StatusCreated, http.StatusNoContent, http.StatusAccepted: + succNE := fmt.Sprintf("%s.%s", neInfo.NeType, neInfo.NeId) + traceTask.SuccNEs = append(traceTask.SuccNEs, succNE) + default: + log.Warnf("Post return code:%d, message:%s", resp.StatusCode(), string(resp.Body())) + failNE := fmt.Sprintf("%s.%s", neInfo.NeType, neInfo.NeId) + traceTask.FailNEs = append(traceTask.FailNEs, failNE) + } + } + } + } + + if len(traceTask.SuccNEs) > 0 { + traceTask.Status = "Active" + _, err = dborm.XormUpdateTableById(traceTask.Id, "trace_task", traceTask) + if err != nil { + log.Error("Failed to dborm.XormUpdateTableById:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + services.ResponseStatusOK204NoContent(w) + } else { + traceTask.Status = "Failed" + _, err = dborm.XormUpdateTableById(traceTask.Id, "trace_task", traceTask) + if err != nil { + log.Error("Failed to dborm.XormUpdateTableById:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + err = global.ErrTraceFailedDistributeToNEs + log.Error(err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } +} + +func PutTraceTaskToNF(w http.ResponseWriter, r *http.Request) { + log.Debug("PutTraceTaskToNF processing... ") + + //vars := mux.Vars(r) + + token, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + log.Debug("AccessToken:", token) + + body, err := io.ReadAll(io.LimitReader(r.Body, int64(config.GetYamlConfig().Params.UriMaxLen))) + if err != nil { + log.Error("io.ReadAll is failed:", err) + services.ResponseNotFound404UriNotExist(w, r) + return + } + + traceTask := new(TraceTask) + _ = json.Unmarshal(body, traceTask) + + traceTask.NotifyUrl = config.GetYamlConfig().OMC.GtpUri + log.Debug("traceTask:", traceTask) + + var neTypes []string + // do not set device + if traceTask.NeType == "" { + // query neType by interface + if len(traceTask.Interfaces) > 0 { + err := dborm.XormGetSingleColStringArrayByIn("trace_info", "ne_type", "interface", traceTask.Interfaces, &neTypes) + if err != nil { + log.Error("Failed to dborm.XormGetSingleColStringArrayByIn:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + } else { + neTypes = []string{"AMF", "SMF", "UDM", "AUSF", "UPF"} + } + + } else { + neTypes = append(neTypes, traceTask.NeType) + } + log.Debug("neTypes:", neTypes) + + for _, neType := range neTypes { + var neInfos []dborm.NeInfo + if traceTask.NeId == "" { + err := dborm.XormGetNeInfoByNeType(neType, &neInfos) + if err != nil { + log.Error("Failed to dborm.XormGetNeInfoByNeType:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + } else { + neInfo, err := dborm.XormGetNeInfo(neType, traceTask.NeId) + if err != nil { + log.Error("Failed to dborm.XormGetNeInfoByNeType:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + neInfos = append(neInfos, *neInfo) + } + for _, neInfo := range neInfos { + hostUri := fmt.Sprintf("http://%s:%v", neInfo.Ip, neInfo.Port) + requestURI2NF := fmt.Sprintf("%s%s", hostUri, UriTraceTaskV1) + log.Debug("requestURI2NF:", requestURI2NF) + + body, _ := json.Marshal(traceTask) + log.Debug("body:", string(body)) + resp, err := client.R(). + EnableTrace(). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + SetBody(body). + Put(requestURI2NF) + if err != nil { + log.Error("Failed to Put:", err) + failNE := fmt.Sprintf("%s.%s", neInfo.NeType, neInfo.NeId) + traceTask.FailNEs = append(traceTask.FailNEs, failNE) + } else { + switch resp.StatusCode() { + case http.StatusOK, http.StatusCreated, http.StatusNoContent, http.StatusAccepted: + succNE := fmt.Sprintf("%s.%s", neInfo.NeType, neInfo.NeId) + traceTask.SuccNEs = append(traceTask.SuccNEs, succNE) + default: + log.Warn("Post return code:%d, message:%s", resp.StatusCode(), string(resp.Body())) + failNE := fmt.Sprintf("%s.%s", neInfo.NeType, neInfo.NeId) + traceTask.FailNEs = append(traceTask.FailNEs, failNE) + } + } + + } + } + + if len(traceTask.SuccNEs) > 0 { + traceTask.Status = "Active" + _, err = dborm.XormUpdateTableById(traceTask.Id, "trace_task", traceTask) + if err != nil { + log.Error("Failed to dborm.XormUpdateTableById:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + services.ResponseStatusOK204NoContent(w) + } else { + traceTask.Status = "Failed" + _, err = dborm.XormUpdateTableById(traceTask.Id, "trace_task", traceTask) + if err != nil { + log.Error("Failed to dborm.XormUpdateTableById:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + err = global.ErrTraceFailedDistributeToNEs + log.Error(err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } +} + +func DeleteTraceTaskToNF(w http.ResponseWriter, r *http.Request) { + log.Debug("DeleteTraceTaskToNF processing... ") + + token, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + log.Debug("AccessToken:", token) + + vars := r.URL.Query() + ids, ok := vars["id"] + if !ok || len(ids) == 0 { + err = global.ErrTraceNotCarriedTaskID + log.Error(err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + log.Debug("ids:", ids) + + for _, id := range ids { + log.Debug("id:", id) + + var succNes []string + err = dborm.XormGetColStringArrayByWhere("trace_task", "succ_nes", fmt.Sprintf("id=%s", id), &succNes) + if err != nil { + log.Error("Failed to dborm.XormGetSingleColStringArrayByWhere:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + log.Debug("succNes:", succNes) + nes := new([]string) + if len(succNes) > 0 { + _ = json.Unmarshal([]byte(succNes[0]), nes) + } + log.Debug("nes:", nes) + + for _, ne := range *nes { + i := strings.Index(ne, ".") + neType := ne[0:i] + neId := ne[i+1:] + log.Debugf("ne:%s neType:%s neId:%s", ne, neType, neId) + neInfo, err := dborm.XormGetNeInfo(neType, neId) + if err != nil { + log.Error("Failed to dborm.XormGetNeInfo:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + + hostUri := fmt.Sprintf("http://%s:%v", neInfo.Ip, neInfo.Port) + requestURI2NF := fmt.Sprintf("%s%s?id=%s", hostUri, UriTraceTaskV1, id) + log.Debug("requestURI2NF:", requestURI2NF) + + _, err = client.R(). + EnableTrace(). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + Delete(requestURI2NF) + if err != nil { + log.Error("Failed to Delete:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + } + + _, err = dborm.XormDeleteDataByWhere(fmt.Sprintf("id=%s", id), "trace_task") + if err != nil { + log.Error("Failed to dborm.XormDeleteDataByWhere:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + } + services.ResponseStatusOK204NoContent(w) +} + +func GetRawMessage(w http.ResponseWriter, r *http.Request) { + log.Debug("GetRawMessage processing... ") + +} + +func ParseRawMsg2Html(w http.ResponseWriter, r *http.Request) { + log.Debug("ParseRawMsg2Html processing... ") + + vars := mux.Vars(r) + idStr := vars["id"] + id, _ := strconv.Atoi(idStr) + + traceData, err := dborm.XormGetTraceData(id) + if err != nil { + log.Error("Failed to dborm.XormGetTraceRawMsg:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + log.Trace("traceData:", traceData) + filePath := traceData.DecMsg + if traceData.DecMsg == "" { + htmlFile := fmt.Sprintf("traceDecMessage-%d-%d.html", traceData.TaskID, traceData.ID) + filePath = config.GetYamlConfig().OMC.FrontTraceDir + "/" + htmlFile + command := fmt.Sprintf("/usr/local/omc/bin/data2html -f %s -t %d -i N%d -d %x", filePath, traceData.Timestamp, traceData.IfType, traceData.RawMsg) + out, err := run.ExecCmd(command, "/") + log.Tracef("Exec output: %v", string(out)) + if err != nil { + log.Errorf("Faile to ipdate2html:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + exist, err := global.FilePathExists(filePath) + if err != nil { + log.Errorf("Failed to stat:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + if !exist { + err = errors.New(string(strings.ReplaceAll(string(out), "\n", ""))) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + traceData.DecMsg = filePath + _, err = dborm.XormUpdateTraceData(id, traceData) + if err != nil { + log.Errorf("Faile to XormUpdateTraceData:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + } + services.ResponseHtmlContent(w, http.StatusOK, filePath) +} diff --git a/features/udm_user/api_udm_user.go b/features/udm_user/api_udm_user.go new file mode 100644 index 0000000..e034acb --- /dev/null +++ b/features/udm_user/api_udm_user.go @@ -0,0 +1,1209 @@ +package udmuser + +import ( + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "ems.agt/features/udm_user/model" + "ems.agt/features/udm_user/service" + "ems.agt/lib/core/conf" + "ems.agt/lib/core/file" + mmlclient "ems.agt/lib/core/mml_client" + "ems.agt/lib/core/utils/ctx" + "ems.agt/lib/core/utils/parse" + "ems.agt/lib/core/vo/result" + "ems.agt/lib/dborm" + "ems.agt/lib/log" + "ems.agt/lib/midware" + "ems.agt/lib/services" + "ems.agt/restagent/config" +) + +// UDM 用户信息接口添加到路由 +func Routers() []services.RouterItem { + // 实例化控制层 SysDictTypeApi 结构体 + var apis = &UdmUserApi{ + authUser: *service.NewServiceUdmAuthUser, + subUser: *service.NewServiceUdmSubUser, + } + + rs := [...]services.RouterItem{ + // UDM签约用户 + { + Method: "GET", + Pattern: "/auths", + Handler: apis.UdmAuthUserList, + Middleware: midware.Authorize(nil), + }, + { + Method: "POST", + Pattern: "/authSave/{neId}", + Handler: apis.UdmAuthUserSave, + Middleware: midware.Authorize(nil), + }, + { + Method: "GET", + Pattern: "/auth/{neId}/{imsi}", + Handler: apis.UdmAuthUserInfo, + Middleware: midware.Authorize(nil), + }, + { + Method: "POST", + Pattern: "/auth/{neId}", + Handler: apis.UdmAuthUserAdd, + Middleware: midware.Authorize(nil), + }, + { + Method: "POST", + Pattern: "/auth/{neId}/{num}", + Handler: apis.UdmAuthUserAdds, + Middleware: midware.Authorize(nil), + }, + { + Method: "PUT", + Pattern: "/auth/{neId}", + Handler: apis.UdmAuthUserEdit, + Middleware: midware.Authorize(nil), + }, + { + Method: "DELETE", + Pattern: "/auth/{neId}/{imsi}", + Handler: apis.UdmAuthUserRemove, + Middleware: midware.Authorize(nil), + }, + { + Method: "DELETE", + Pattern: "/auth/{neId}/{imsi}/{num}", + Handler: apis.UdmAuthUserRemoves, + Middleware: midware.Authorize(nil), + }, + { + Method: "POST", + Pattern: "/authExport", + Handler: apis.UdmAuthUserExport, + Middleware: midware.Authorize(nil), + }, + { + Method: "POST", + Pattern: "/authImport/{neId}", + Handler: apis.UdmAuthUserImport, + Middleware: midware.Authorize(nil), + }, + // UDM签约用户 + { + Method: "GET", + Pattern: "/subs", + Handler: apis.UdmSubUserList, + Middleware: midware.Authorize(nil), + }, + { + Method: "POST", + Pattern: "/subSave/{neId}", + Handler: apis.UdmSubUserSave, + Middleware: midware.Authorize(nil), + }, + { + Method: "GET", + Pattern: "/subInfo/{neId}/{imsi}", + Handler: apis.UdmSubUserInfo, + Middleware: midware.Authorize(nil), + }, + { + Method: "POST", + Pattern: "/sub/{neId}", + Handler: apis.UdmSubUserAdd, + Middleware: midware.Authorize(nil), + }, + { + Method: "POST", + Pattern: "/sub/{neId}/{num}", + Handler: apis.UdmSubUserAdds, + Middleware: midware.Authorize(nil), + }, + { + Method: "PUT", + Pattern: "/sub/{neId}", + Handler: apis.UdmSubUserEdit, + Middleware: midware.Authorize(nil), + }, + { + Method: "PUT", + Pattern: "/sub4gIP/{neId}", + Handler: apis.UdmSubUser4GIP, + Middleware: midware.Authorize(nil), + }, + { + Method: "PUT", + Pattern: "/subSmData/{neId}", + Handler: apis.UdmSubUserSmData, + Middleware: midware.Authorize(nil), + }, + { + Method: "DELETE", + Pattern: "/sub/{neId}/{imsi}", + Handler: apis.UdmSubUserRemove, + Middleware: midware.Authorize(nil), + }, + { + Method: "DELETE", + Pattern: "/sub/{neId}/{imsi}/{num}", + Handler: apis.UdmSubUserRemoves, + Middleware: midware.Authorize(nil), + }, + { + Method: "POST", + Pattern: "/subExport", + Handler: apis.UdmSubUserExport, + Middleware: midware.Authorize(nil), + }, + { + Method: "POST", + Pattern: "/subImport/{neId}", + Handler: apis.UdmSubUserImport, + Middleware: midware.Authorize(nil), + }, + // 添加更多的 Router 对象... + } + + // 生成两组前缀路由 + rsPrefix := []services.RouterItem{} + for _, v := range rs { + path := "/udmUserManage/{apiVersion}" + v.Pattern + // 固定前缀 + v.Pattern = config.DefaultUriPrefix + path + rsPrefix = append(rsPrefix, v) + // 可配置 + v.Pattern = config.UriPrefix + path + rsPrefix = append(rsPrefix, v) + } + return rsPrefix +} + +// NeInfoByUDM 网元信息 +func NeInfoByUDM(neId string) (*dborm.NeInfo, error) { + neInfo, err := dborm.XormGetNeInfo("UDM", neId) + if err != nil { + log.Error("dborm.XormGetNeInfo is failed:", err) + return nil, err + } + if neInfo == nil || neInfo.Ip == "" { + return nil, fmt.Errorf("not ne_info or not IP") + } + return neInfo, nil +} + +// UDM 用户 +// +// PATH /udmUserManage +type UdmUserApi struct { + authUser service.ServiceUdmAuthUser + subUser service.ServiceUdmSubUser +} + +// UDM鉴权用户 +// +// GET /auths +func (s *UdmUserApi) UdmAuthUserList(w http.ResponseWriter, r *http.Request) { + querys := ctx.QueryMap(r) + querys["neId"] = "" + data := s.authUser.Page(querys) + // 遍历安全掩码 + rows := data["rows"].([]model.UdmAuthUser) + maskRows := []model.UdmAuthUser{} + for _, v := range rows { + v.Ki = parse.SafeContent(v.Ki) + v.Opc = parse.SafeContent(v.Opc) + maskRows = append(maskRows, v) + } + data["rows"] = maskRows + ctx.JSON(w, 200, result.Ok(data)) +} + +// UDM鉴权用户-获取全部保存数据库 +// +// POST /authSave/{neId} +func (s *UdmUserApi) UdmAuthUserSave(w http.ResponseWriter, r *http.Request) { + neId := ctx.Param(r, "neId") + if neId == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + neId = "" + data := s.authUser.Save(neId) + ctx.JSON(w, 200, result.OkData(data)) +} + +// UDM鉴权用户-信息 +// +// GET /authInfo/{neId}/{imsi} +func (s *UdmUserApi) UdmAuthUserInfo(w http.ResponseWriter, r *http.Request) { + neId := ctx.Param(r, "neId") + imsi := ctx.Param(r, "imsi") + if neId == "" || imsi == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + neInfo, err := NeInfoByUDM(neId) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + msg := fmt.Sprintf("dsp authdat:imsi=%s", imsi) + + // 发送MML + data, err := mmlclient.MMLSendMsgToMap(neInfo.Ip, msg) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + neId = "" + var userInfo model.UdmAuthUser + list := s.authUser.List(model.UdmAuthUser{NeID: neId, Imsi: imsi}) + if len(list) > 0 { + userInfo = list[0] + } else { + userInfo = model.UdmAuthUser{ + Imsi: imsi, + Amf: data["amf"], + AlgoIndex: data["algo"], + Opc: data["opc"], + Ki: data["ki"], + } + + s.authUser.Insert(neId, userInfo) + } + userInfo.Ki = parse.SafeContent(userInfo.Ki) + userInfo.Opc = parse.SafeContent(userInfo.Opc) + ctx.JSON(w, 200, result.OkData(userInfo)) + +} + +// UDM鉴权用户-增加 +// +// POST /{neId} +func (s *UdmUserApi) UdmAuthUserAdd(w http.ResponseWriter, r *http.Request) { + neId := ctx.Param(r, "neId") + if neId == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + var body model.UdmAuthUser + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.Imsi == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + neInfo, err := NeInfoByUDM(neId) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + msg := fmt.Sprintf("add authdat:imsi=%s,ki=%s,amf=%s,algo=%s,opc=%s", body.Imsi, body.Ki, body.Amf, body.AlgoIndex, body.Opc) + + // 发送MML + data, err := mmlclient.MMLSendMsgToString(neInfo.Ip, msg) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + // 命令ok时 + if strings.Contains(data, "ok") { + neId = "" + s.authUser.Insert(neId, body) + } + ctx.JSON(w, 200, result.OkData(data)) +} + +// UDM鉴权用户-批量添加 +// +// POST /{neId}/{num} +func (s *UdmUserApi) UdmAuthUserAdds(w http.ResponseWriter, r *http.Request) { + neId := ctx.Param(r, "neId") + num := ctx.Param(r, "num") + if neId == "" || num == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + var body model.UdmAuthUser + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.Imsi == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + neInfo, err := NeInfoByUDM(neId) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + msg := fmt.Sprintf("bad authdat:start_imsi=%s,sub_num=%s,ki=%s,amf=%s,algo=%s", body.Imsi, num, body.Ki, body.Amf, body.AlgoIndex) + + // 发送MML + data, err := mmlclient.MMLSendMsgToString(neInfo.Ip, msg) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + // 命令ok时 + if strings.Contains(data, "ok") { + neId = "" + s.authUser.Inserts(neId, body, num) + } + ctx.JSON(w, 200, result.OkData(data)) +} + +// UDM鉴权用户-修改 +// +// PUT /{neId} +func (s *UdmUserApi) UdmAuthUserEdit(w http.ResponseWriter, r *http.Request) { + neId := ctx.Param(r, "neId") + if neId == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + var body model.UdmAuthUser + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.Imsi == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + neInfo, err := NeInfoByUDM(neId) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + msg := fmt.Sprintf("mod authdata:imsi=%s", body.Imsi) + // 修改的参数名称 + if body.Ki != "" { + msg += fmt.Sprintf(",ki=%s", body.Ki) + } + if body.Amf != "" { + msg += fmt.Sprintf(",amf=%s", body.Amf) + } + if body.AlgoIndex != "" { + msg += fmt.Sprintf(",algo=%s", body.AlgoIndex) + } + if body.Opc != "" { + msg += fmt.Sprintf(",opc=%s", body.Opc) + } + + // 发送MML + data, err := mmlclient.MMLSendMsgToString(neInfo.Ip, msg) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + // 命令ok时 + if strings.Contains(data, "ok") { + neId = "" + s.authUser.Update(neId, body) + } + ctx.JSON(w, 200, result.OkData(data)) +} + +// UDM鉴权用户-删除 +// +// DELETE /{neId}/{imsi} +func (s *UdmUserApi) UdmAuthUserRemove(w http.ResponseWriter, r *http.Request) { + neId := ctx.Param(r, "neId") + imsi := ctx.Param(r, "imsi") + if neId == "" || imsi == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + neInfo, err := NeInfoByUDM(neId) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + msg := fmt.Sprintf("del authdat:imsi=%s", imsi) + + // 发送MML + data, err := mmlclient.MMLSendMsgToString(neInfo.Ip, msg) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + // 命令ok时 + if strings.Contains(data, "ok") { + neId = "" + s.authUser.Delete(neId, imsi) + } + ctx.JSON(w, 200, result.OkData(data)) +} + +// UDM鉴权用户-批量删除 +// +// DELETE /{neId}/{imsi}/{num} +func (s *UdmUserApi) UdmAuthUserRemoves(w http.ResponseWriter, r *http.Request) { + neId := ctx.Param(r, "neId") + imsi := ctx.Param(r, "imsi") + num := ctx.Param(r, "num") + if neId == "" || imsi == "" || num == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + neInfo, err := NeInfoByUDM(neId) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + msg := fmt.Sprintf("bde authdat:start_imsi=%s,sub_num=%s", imsi, num) + + // 发送MML + data, err := mmlclient.MMLSendMsgToString(neInfo.Ip, msg) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + // 命令ok时 + if strings.Contains(data, "ok") { + neId = "" + s.authUser.Deletes(neId, imsi, num) + } + ctx.JSON(w, 200, result.OkData(data)) +} + +// UDM鉴权用户-导出 +// +// POST /authExport +func (s *UdmUserApi) UdmAuthUserExport(w http.ResponseWriter, r *http.Request) { + var body struct { + NeId string `json:"neId"` + Type string `json:"type"` + } + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.NeId == "" || body.Type == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + if !(body.Type == "csv" || body.Type == "txt") { + ctx.JSON(w, 200, result.ErrMsg("导出文件类型支持csv、txt")) + return + } + + neId := "-" + list := s.authUser.List(model.UdmAuthUser{NeID: neId}) + // 文件名 + fileName := fmt.Sprintf("OMC_AUTH_USER_EXPORT_%s_%d.%s", neId, time.Now().UnixMilli(), body.Type) + filePath := fmt.Sprintf("%s/upload/mml/%s", conf.Get("ne.omcdir"), fileName) + + if body.Type == "csv" { + // 转换数据 + data := [][]string{} + data = append(data, []string{"imsi", "ki", "amf", "algo", "opc"}) + for _, v := range list { + maskKi := parse.SafeContent(v.Ki) + maskOpc := parse.SafeContent(v.Opc) + data = append(data, []string{v.Imsi, maskKi, v.Amf, v.AlgoIndex, maskOpc}) + } + // 输出到文件 + err := file.WriterCSVFile(data, filePath) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + } + + if body.Type == "txt" { + // 转换数据 + data := [][]string{} + for _, v := range list { + maskKi := parse.SafeContent(v.Ki) + maskOpc := parse.SafeContent(v.Opc) + data = append(data, []string{v.Imsi, maskKi, v.Amf, v.AlgoIndex, maskOpc}) + } + // 输出到文件 + err = file.WriterTxtFile(data, filePath) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + } + + ctx.FileAttachment(w, r, filePath, fileName) +} + +// UDM鉴权用户-导入 +// +// POST /authImport/{neId} +func (s *UdmUserApi) UdmAuthUserImport(w http.ResponseWriter, r *http.Request) { + neId := ctx.Param(r, "neId") + if neId == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + // 获取文件名 + _, fileHeader, err := r.FormFile("file") + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + if !(strings.HasSuffix(fileHeader.Filename, ".csv") || strings.HasSuffix(fileHeader.Filename, ".txt")) { + ctx.JSON(w, 200, result.ErrMsg("请上传.csv、.txt格式文件,内容字段imsi,ki,algo,amf,opc")) + return + } + + neInfo, err := NeInfoByUDM(neId) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + // 文件名 + fileName := fmt.Sprintf("OMC_AUTH_USER_IMPORT_%s_%d_%s", neId, time.Now().UnixMilli(), fileHeader.Filename) + localPath := fmt.Sprintf("%s/upload/mml/%s", conf.Get("ne.omcdir"), fileName) + nePath := conf.Get("mml.upload").(string) + // 输出保存文件 + err = ctx.SaveUploadedFile(r, localPath) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + // 复制到远程 + err = file.FileSCPLocalToNe(neInfo.Ip, localPath, nePath) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + msg := fmt.Sprintf("import authdat:path=%s", fmt.Sprintf("%s/%s", nePath, fileName)) + + // 发送MML + data, err := mmlclient.MMLSendMsgToString(neInfo.Ip, msg) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + // 命令ok时 + if strings.Contains(data, "ok") { + if strings.HasSuffix(fileHeader.Filename, ".csv") { + data := file.ReadCSVFile(localPath) + neId = "" + go s.authUser.InsertCSV(neId, data) + } + if strings.HasSuffix(fileHeader.Filename, ".txt") { + data := file.ReadTxtFile(localPath) + neId = "" + go s.authUser.InsertTxt(neId, data) + } + } + ctx.JSON(w, 200, result.OkData(data)) +} + +// UDM签约用户 +// +// GET /subs +func (s *UdmUserApi) UdmSubUserList(w http.ResponseWriter, r *http.Request) { + querys := ctx.QueryMap(r) + querys["neId"] = "" + data := s.subUser.Page(querys) + ctx.JSON(w, 200, result.Ok(data)) +} + +// UDM签约用户-获取全部保存数据库 +// +// POST /subSave/{neId} +func (s *UdmUserApi) UdmSubUserSave(w http.ResponseWriter, r *http.Request) { + neId := ctx.Param(r, "neId") + if neId == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + neId = "" + data := s.subUser.Save(neId) + ctx.JSON(w, 200, result.OkData(data)) +} + +// UDM签约用户-信息 +// +// GET /subInfo/{neId}/{imsi} +func (s *UdmUserApi) UdmSubUserInfo(w http.ResponseWriter, r *http.Request) { + neId := ctx.Param(r, "neId") + imsi := ctx.Param(r, "imsi") + if neId == "" || imsi == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + neInfo, err := NeInfoByUDM(neId) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + msg := fmt.Sprintf("dsp udmuser:imsi=%s", imsi) + + // 发送MML + data, err := mmlclient.MMLSendMsgToMap(neInfo.Ip, msg) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + neId = "" + var userInfo model.UdmSubUser + list := s.subUser.List(model.UdmSubUser{NeID: neId, Imsi: imsi}) + if len(list) > 0 { + userInfo = list[0] + } else { + cnType, _ := strconv.ParseInt(data["CNType"][:4], 0, 64) + rat, _ := strconv.ParseInt(data["RAT"][:4], 0, 64) + userInfo = model.UdmSubUser{ + Imsi: imsi, + Msisdn: data["MSISDN"], + Ambr: data["AMBR"], + Arfb: data["AreaForbidden"], + Cn: fmt.Sprint(cnType), + SmData: data["SM-Data(snssai+dnn[1..n])"], + Sar: data["ServiceAreaRestriction"], + Nssai: data["NSSAI"], + SmfSel: data["Smf-Selection"], + Rat: fmt.Sprint(rat), + } + // 1,64,24,65,def_eps,1,2,010200000000,- + if v, ok := data["EPS-Data"]; ok { + userInfo.EpsDat = v + arr := strings.Split(v, ",") + userInfo.EpsFlag = arr[0] + userInfo.EpsOdb = arr[1] + userInfo.HplmnOdb = arr[2] + userInfo.Ard = arr[3] + userInfo.Epstpl = arr[4] + userInfo.ContextId = arr[5] + userInfo.ApnContext = arr[7] + userInfo.StaticIp = arr[8] + } + + s.subUser.Insert(neId, userInfo) + } + ctx.JSON(w, 200, result.OkData(userInfo)) +} + +// UDM签约用户-增加 +// +// POST /{neId} +func (s *UdmUserApi) UdmSubUserAdd(w http.ResponseWriter, r *http.Request) { + neId := ctx.Param(r, "neId") + if neId == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + var body model.UdmSubUser + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.Imsi == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + neInfo, err := NeInfoByUDM(neId) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + msg := fmt.Sprintf("add udmuser:imsi=%s,msisdn=%s,ambr=%s,nssai=%s,arfb=%s,sar=%s,rat=%s,cn=%s,smf_sel=%s,sm_data=%s,eps_flag=%s,eps_odb=%s,hplmn_odb=%s,ard=%s,epstpl=%s,context_id=%s,apn_context=%s", + body.Imsi, body.Msisdn, body.Ambr, body.Nssai, body.Arfb, body.Sar, body.Rat, body.Cn, body.SmfSel, body.SmData, body.EpsFlag, body.EpsOdb, body.HplmnOdb, body.Ard, body.Epstpl, body.ContextId, body.ApnContext) + // static_ip指给4G UE分配的静态IP,没有可不带此字段名,批量添加IP会自动递增 + if body.StaticIp != "" { + msg += fmt.Sprintf(",static_ip=%s", body.StaticIp) + } + + // 发送MML + data, err := mmlclient.MMLSendMsgToString(neInfo.Ip, msg) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + // 命令ok时 + if strings.Contains(data, "ok") { + neId = "" + s.subUser.Insert(neId, body) + } + ctx.JSON(w, 200, result.OkData(data)) +} + +// UDM签约用户-批量添加 +// +// POST /{neId}/{num} +func (s *UdmUserApi) UdmSubUserAdds(w http.ResponseWriter, r *http.Request) { + neId := ctx.Param(r, "neId") + num := ctx.Param(r, "num") + if neId == "" || num == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + var body model.UdmSubUser + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.Imsi == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + neInfo, err := NeInfoByUDM(neId) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + msg := fmt.Sprintf("bad udmuser:start_imsi=%s,start_msisdn=%s,sub_num=%s,ambr=%s,nssai=%s,arfb=%s,sar=%s,rat=%s,cn=%s,smf_sel=%s,sm_data=%s,eps_flag=%s,eps_odb=%s,hplmn_odb=%s,ard=%s,epstpl=%s,context_id=%s,apn_context=%s", + body.Imsi, body.Msisdn, num, body.Ambr, body.Nssai, body.Arfb, body.Sar, body.Rat, body.Cn, body.SmfSel, body.SmData, body.EpsFlag, body.EpsOdb, body.HplmnOdb, body.Ard, body.Epstpl, body.ContextId, body.ApnContext) + // static_ip指给4G UE分配的静态IP,没有可不带此字段名,批量添加IP会自动递增 + if body.StaticIp != "" { + msg += fmt.Sprintf(",static_ip=%s", body.StaticIp) + } + + // 发送MML + data, err := mmlclient.MMLSendMsgToString(neInfo.Ip, msg) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + // 命令ok时 + if strings.Contains(data, "ok") { + neId = "" + s.subUser.Inserts(neId, body, num) + } + ctx.JSON(w, 200, result.OkData(data)) +} + +// UDM签约用户-批量添加4G用户 +// +// POST /sub4G/{neId} +func (s *UdmUserApi) UdmSubUserAdd4G(w http.ResponseWriter, r *http.Request) { + neId := ctx.Param(r, "neId") + if neId == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + var body model.UdmSubUser + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.Imsi == "" || body.SubNum == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + neInfo, err := NeInfoByUDM(neId) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + msg := fmt.Sprintf("bmd udmuser:start_imsi=%s,sub_num=%s,eps_flag=%s", body.Imsi, body.SubNum, body.EpsFlag) + + // 发送MML + data, err := mmlclient.MMLSendMsgToString(neInfo.Ip, msg) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + // 命令ok时 + if strings.Contains(data, "ok") { + neId = "" + s.subUser.Insert4G(neId, body) + } + ctx.JSON(w, 200, result.OkData(data)) +} + +// UDM签约用户-修改 +// +// PUT /{neId} +func (s *UdmUserApi) UdmSubUserEdit(w http.ResponseWriter, r *http.Request) { + neId := ctx.Param(r, "neId") + if neId == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + var body model.UdmSubUser + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.Imsi == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + neInfo, err := NeInfoByUDM(neId) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + msg := fmt.Sprintf("mod udmuser:imsi=%s", body.Imsi) + // 修改的参数名称 + if body.Msisdn != "" { + msg += fmt.Sprintf(",msisdn=%s", body.Msisdn) + } + if body.Ambr != "" { + msg += fmt.Sprintf(",ambr=%s", body.Ambr) + } + if body.Nssai != "" { + msg += fmt.Sprintf(",nssai=%s", body.Nssai) + } + if body.Arfb != "" { + msg += fmt.Sprintf(",arfb=%s", body.Arfb) + } + if body.Sar != "" { + msg += fmt.Sprintf(",sar=%s", body.Sar) + } + if body.Rat != "" { + msg += fmt.Sprintf(",rat=%s", body.Rat) + } + if body.Cn != "" { + msg += fmt.Sprintf(",cn=%s", body.Cn) + } + if body.SmfSel != "" { + msg += fmt.Sprintf(",smf_sel=%s", body.SmfSel) + } + if body.SmData != "" { + msg += fmt.Sprintf(",sm_data=%s", body.SmData) + } + if body.EpsDat != "" { + msg += fmt.Sprintf(",eps_dat=%s", body.EpsDat) + } + if body.EpsFlag != "" { + msg += fmt.Sprintf(",eps_flag=%s", body.EpsFlag) + } + if body.EpsOdb != "" { + msg += fmt.Sprintf(",eps_odb=%s", body.EpsOdb) + } + if body.HplmnOdb != "" { + msg += fmt.Sprintf(",hplmn_odb=%s", body.HplmnOdb) + } + if body.Epstpl != "" { + msg += fmt.Sprintf(",epstpl=%s", body.Epstpl) + } + if body.Ard != "" { + msg += fmt.Sprintf(",ard=%s", body.Ard) + } + if body.ContextId != "" { + msg += fmt.Sprintf(",context_id=%s", body.ContextId) + } + if body.ApnContext != "" { + msg += fmt.Sprintf(",apn_context=%s", body.ApnContext) + } + if body.StaticIp != "" { + msg += fmt.Sprintf(",static_ip=%s", body.StaticIp) + } + + // 发送MML + data, err := mmlclient.MMLSendMsgToString(neInfo.Ip, msg) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + // 命令ok时 + if strings.Contains(data, "ok") { + neId = "" + s.subUser.Update(neId, body) + } + ctx.JSON(w, 200, result.OkData(data)) +} + +// UDM签约用户-批量修改4G IP +// +// PUT /sub4gIP/{neId} +func (s *UdmUserApi) UdmSubUser4GIP(w http.ResponseWriter, r *http.Request) { + neId := ctx.Param(r, "neId") + if neId == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + var body model.UdmSubUser + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.Imsi == "" || body.SubNum == "" || body.StaticIp == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + neInfo, err := NeInfoByUDM(neId) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + msg := fmt.Sprintf("bmd udmuser:start_imsi=%s,sub_num=%s,static_ip=%s", body.Imsi, body.SubNum, body.StaticIp) + + // 发送MML + data, err := mmlclient.MMLSendMsgToString(neInfo.Ip, msg) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + // 命令ok时 + if strings.Contains(data, "ok") { + neId = "" + s.subUser.Update4GIP(neId, body) + } + ctx.JSON(w, 200, result.OkData(data)) +} + +// UDM签约用户-批量修改sm-data +// +// PUT /subSmData/{neId} +func (s *UdmUserApi) UdmSubUserSmData(w http.ResponseWriter, r *http.Request) { + neId := ctx.Param(r, "neId") + if neId == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + var body model.UdmSubUser + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.Imsi == "" || body.SubNum == "" || body.SmData == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + neInfo, err := NeInfoByUDM(neId) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + // 3-000003&internet-10.10.1.1&ims-10.11.1.1 + msg := fmt.Sprintf("bmd udmuser:start_imsi=%s,sub_num=%s,sm_data=%s", body.Imsi, body.SubNum, body.SmData) + + // 发送MML + data, err := mmlclient.MMLSendMsgToString(neInfo.Ip, msg) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + // 命令ok时 + if strings.Contains(data, "ok") { + neId = "" + s.subUser.UpdateSmData(neId, body) + } + ctx.JSON(w, 200, result.OkData(data)) +} + +// UDM签约用户-删除 +// +// DELETE /{neId}/{imsi} +func (s *UdmUserApi) UdmSubUserRemove(w http.ResponseWriter, r *http.Request) { + neId := ctx.Param(r, "neId") + imsi := ctx.Param(r, "imsi") + if neId == "" || imsi == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + neInfo, err := NeInfoByUDM(neId) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + msg := fmt.Sprintf("del udmuser:imsi=%s", imsi) + + // 发送MML + data, err := mmlclient.MMLSendMsgToString(neInfo.Ip, msg) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + // 命令ok时 + if strings.Contains(data, "ok") { + neId = "" + s.subUser.Delete(neId, imsi) + } + ctx.JSON(w, 200, result.OkData(data)) +} + +// UDM签约用户-批量删除 +// +// DELETE /{neId}/{imsi}/{num} +func (s *UdmUserApi) UdmSubUserRemoves(w http.ResponseWriter, r *http.Request) { + neId := ctx.Param(r, "neId") + imsi := ctx.Param(r, "imsi") + num := ctx.Param(r, "num") + if neId == "" || imsi == "" || num == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + neInfo, err := NeInfoByUDM(neId) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + msg := fmt.Sprintf("bde udmuser:start_imsi=%s,sub_num=%s", imsi, num) + + // 发送MML + data, err := mmlclient.MMLSendMsgToString(neInfo.Ip, msg) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + // 命令ok时 + if strings.Contains(data, "ok") { + neId = "" + s.subUser.Deletes(neId, imsi, num) + } + ctx.JSON(w, 200, result.OkData(data)) +} + +// UDM签约用户-导出 +// +// POST /subExport +func (s *UdmUserApi) UdmSubUserExport(w http.ResponseWriter, r *http.Request) { + var body struct { + NeId string `json:"neId"` + Type string `json:"type"` + } + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.NeId == "" || body.Type == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + if !(body.Type == "csv" || body.Type == "txt") { + ctx.JSON(w, 200, result.ErrMsg("导出文件类型支持csv、txt")) + return + } + + neId := "-" + list := s.subUser.List(model.UdmSubUser{NeID: neId}) + // 文件名 + fileName := fmt.Sprintf("OMC_AUTH_USER_EXPORT_%s_%d.%s", neId, time.Now().UnixMilli(), body.Type) + filePath := fmt.Sprintf("%s/upload/mml/%s", conf.Get("ne.omcdir"), fileName) + + if body.Type == "csv" { + // 转换数据 + data := [][]string{} + data = append(data, []string{"imsi", "msisdn", "ambr", "arfb", "sar", "rat", "cn", "smf_sel", "sm_dat", "eps_dat"}) + for _, v := range list { + data = append(data, []string{v.Imsi, v.Msisdn, v.Ambr, v.Arfb, v.Sar, v.Rat, v.Cn, v.SmfSel, v.SmData, v.EpsDat}) + } + // 输出到文件 + err = file.WriterCSVFile(data, filePath) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + } + + if body.Type == "txt" { + // 转换数据 + data := [][]string{} + for _, v := range list { + data = append(data, []string{v.Imsi, v.Msisdn, v.Ambr, v.Arfb, v.Sar, v.Rat, v.Cn, v.SmfSel, v.SmData, v.EpsDat}) + } + // 输出到文件 + err = file.WriterTxtFile(data, filePath) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + } + + ctx.FileAttachment(w, r, filePath, fileName) +} + +// UDM签约用户-导入 +// +// POST /subImport/{neId} +func (s *UdmUserApi) UdmSubUserImport(w http.ResponseWriter, r *http.Request) { + neId := ctx.Param(r, "neId") + if neId == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + // 获取文件名 + _, fileHeader, err := r.FormFile("file") + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + if !(strings.HasSuffix(fileHeader.Filename, ".csv") || strings.HasSuffix(fileHeader.Filename, ".txt")) { + ctx.JSON(w, 200, result.ErrMsg("请上传.csv、.txt格式文件,内容字段imsi,msisdn,ambr,nssai,arfb,sar,rat,cn,smf_sel,sm_dat,eps_dat")) + return + } + + neInfo, err := NeInfoByUDM(neId) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + // 文件名 + fileName := fmt.Sprintf("OMC_SUB_USER_IMPORT_%s_%d_%s", neId, time.Now().UnixMilli(), fileHeader.Filename) + localPath := fmt.Sprintf("%s/upload/mml/%s", conf.Get("ne.omcdir"), fileName) + nePath := conf.Get("mml.upload").(string) + // 输出保存文件 + err = ctx.SaveUploadedFile(r, localPath) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + // 复制到远程 + err = file.FileSCPLocalToNe(neInfo.Ip, localPath, nePath) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + + msg := fmt.Sprintf("import udmuser:path=%s", fmt.Sprintf("%s/%s", nePath, fileName)) + + // 发送MML + data, err := mmlclient.MMLSendMsgToString(neInfo.Ip, msg) + if err != nil { + ctx.JSON(w, 200, result.ErrMsg(err.Error())) + return + } + // 命令ok时 + if strings.Contains(data, "ok") { + if strings.HasSuffix(fileHeader.Filename, ".csv") { + data := file.ReadCSVFile(localPath) + neId = "" + go s.subUser.InsertCSV(neId, data) + } + if strings.HasSuffix(fileHeader.Filename, ".txt") { + data := file.ReadTxtFile(localPath) + neId = "" + go s.subUser.InsertTxt(neId, data) + } + } + ctx.JSON(w, 200, result.OkData(data)) +} diff --git a/features/udm_user/model/udm_auth_user.go b/features/udm_user/model/udm_auth_user.go new file mode 100644 index 0000000..520d346 --- /dev/null +++ b/features/udm_user/model/udm_auth_user.go @@ -0,0 +1,13 @@ +package model + +// UdmAuthUser UDM鉴权用户 +type UdmAuthUser struct { + ID string `json:"id" xorm:"pk 'id' autoincr"` + Imsi string `json:"imsi" xorm:"imsi"` // SIM卡号 + Amf string `json:"amf" xorm:"amf"` // ANF + Status string `json:"status" xorm:"status"` // 状态 + Ki string `json:"ki" xorm:"ki"` // ki + AlgoIndex string `json:"algoIndex" xorm:"algo_index"` // + Opc string `json:"opc" xorm:"opc"` + NeID string `json:"neId" xorm:"ne_id"` // UDM网元标识-子系统 +} diff --git a/features/udm_user/model/udm_sub_user.go b/features/udm_user/model/udm_sub_user.go new file mode 100644 index 0000000..466fce1 --- /dev/null +++ b/features/udm_user/model/udm_sub_user.go @@ -0,0 +1,29 @@ +package model + +// UdmSubUser UDM签约用户 +type UdmSubUser struct { + ID string `json:"id" xorm:"pk 'id' autoincr"` + Msisdn string `json:"msisdn" xorm:"msisdn"` // 相当手机号 + Imsi string `json:"imsi" xorm:"imsi"` // SIM卡号 + Ambr string `json:"ambr" xorm:"ambr"` + Nssai string `json:"nssai" xorm:"nssai"` + Rat string `json:"rat" xorm:"rat"` + Arfb string `json:"arfb" xorm:"arfb"` + Sar string `json:"sar" xorm:"sar"` + Cn string `json:"cn" xorm:"cn"` + SmData string `json:"smData" xorm:"sm_data"` + SmfSel string `json:"smfSel" xorm:"smf_sel"` + EpsDat string `json:"epsDat" xorm:"eps_dat"` + NeID string `json:"neId" xorm:"ne_id"` // UDM网元标识-子系统 + + EpsFlag string `json:"epsFlag" xorm:"eps_flag"` + EpsOdb string `json:"epsOdb" xorm:"eps_odb"` + HplmnOdb string `json:"hplmnOdb" xorm:"hplmn_odb"` + Ard string `json:"ard" xorm:"ard"` + Epstpl string `json:"epstpl" xorm:"epstpl"` + ContextId string `json:"contextId" xorm:"context_id"` + ApnContext string `json:"apnContext" xorm:"apn_context"` + StaticIp string `json:"staticIp" xorm:"static_ip"` + + SubNum string `json:"subNum,omitempty" xorm:"-"` // 批量数 +} diff --git a/features/udm_user/repo/repo_udm_auth_user.go b/features/udm_user/repo/repo_udm_auth_user.go new file mode 100644 index 0000000..1f3bf67 --- /dev/null +++ b/features/udm_user/repo/repo_udm_auth_user.go @@ -0,0 +1,274 @@ +package repo + +import ( + "strconv" + "strings" + + "ems.agt/features/udm_user/model" + "ems.agt/lib/core/datasource" + "ems.agt/lib/core/utils/parse" + "ems.agt/lib/log" +) + +// 实例化数据层 RepoUdmAuthUser 结构体 +var NewRepoUdmAuthUser = &RepoUdmAuthUser{ + selectSql: `select + id, imsi, amf, status, ki, algo_index, opc, ne_id + from u_auth_user`, + + resultMap: map[string]string{ + "id": "ID", + "imsi": "Imsi", + "amf": "Amf", + "status": "Status", + "ki": "Ki", + "algo_index": "AlgoIndex", + "opc": "Opc", + "ne_id": "NeID", + }, +} + +// RepoUdmAuthUser UDM鉴权用户 数据层处理 +type RepoUdmAuthUser struct { + // 查询视图对象SQL + selectSql string + // 结果字段与实体映射 + resultMap map[string]string +} + +// convertResultRows 将结果记录转实体结果组 +func (r *RepoUdmAuthUser) convertResultRows(rows []map[string]any) []model.UdmAuthUser { + arr := make([]model.UdmAuthUser, 0) + for _, row := range rows { + UdmUser := model.UdmAuthUser{} + for key, value := range row { + if keyMapper, ok := r.resultMap[key]; ok { + datasource.SetFieldValue(&UdmUser, keyMapper, value) + } + } + arr = append(arr, UdmUser) + } + return arr +} + +// SelectPage 根据条件分页查询 +func (r *RepoUdmAuthUser) SelectPage(query map[string]any) map[string]any { + // 查询条件拼接 + var conditions []string + var params []any + if v, ok := query["imsi"]; ok && v != "" { + conditions = append(conditions, "imsi like concat(concat('%', ?), '%')") + params = append(params, v) + } + if v, ok := query["neId"]; ok && v != "" { + conditions = append(conditions, "ne_id = ?") + params = append(params, v) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + result := map[string]any{ + "total": 0, + "rows": []model.UdmAuthUser{}, + } + + // 查询数量 长度为0直接返回 + totalSql := "select count(1) as 'total' from u_auth_user" + totalRows, err := datasource.RawDB("", totalSql+whereSql, params) + if err != nil { + log.Errorf("total err => %v", err) + return result + } + total := parse.Number(totalRows[0]["total"]) + if total == 0 { + return result + } else { + result["total"] = total + } + + // 分页 + pageNum, pageSize := datasource.PageNumSize(query["pageNum"], query["pageSize"]) + pageSql := " limit ?,? " + params = append(params, pageNum*pageSize) + params = append(params, pageSize) + + // 排序 + sortSql := "" + if v, ok := query["sortField"]; ok && v != "" { + if v == "imsi" { + sortSql += " order by imsi " + } + if v, ok := query["sortOrder"]; ok && v != nil { + if v == "desc" { + sortSql += " desc " + } else { + sortSql += " asc " + } + } + } + + // 查询数据 + querySql := r.selectSql + whereSql + sortSql + pageSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + log.Errorf("query err => %v", err) + } + + // 转换实体 + result["rows"] = r.convertResultRows(results) + return result +} + +// SelectList 根据实体查询 +func (r *RepoUdmAuthUser) SelectList(auth model.UdmAuthUser) []model.UdmAuthUser { + // 查询条件拼接 + var conditions []string + var params []any + if auth.Imsi != "" { + conditions = append(conditions, "imsi = ?") + params = append(params, auth.Imsi) + } + if auth.NeID != "" { + conditions = append(conditions, "ne_id = ?") + params = append(params, auth.NeID) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询数据 + querySql := r.selectSql + whereSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + log.Errorf("query err => %v", err) + } + + // 转换实体 + return r.convertResultRows(results) +} + +// ClearAndInsert 清空ne_id后新增实体 +func (r *RepoUdmAuthUser) ClearAndInsert(neID string, authArr []model.UdmAuthUser) int64 { + var num int64 = 0 + + // 清空指定ne_id + _, err := datasource.ExecDB("", "TRUNCATE TABLE u_auth_user", nil) + // _, err := datasource.ExecDB("", "DELETE FROM u_auth_user WHERE ne_id = ?", []any{neID}) + if err != nil { + log.Errorf("TRUNCATE err => %v", err) + } + + n := len(authArr) + batchSize := 2000 + for i := 0; i < n; i += batchSize { + end := i + batchSize + if end > n { + end = n + } + batch := authArr[i:end] + + // 调用 InsertMulti 函数将批量数据插入数据库 + results, err := datasource.DefaultDB().Table("u_auth_user").InsertMulti(batch) + if err != nil { + continue + } + num += results + } + return num +} + +// Insert 新增实体 +func (r *RepoUdmAuthUser) Insert(authUser model.UdmAuthUser) int64 { + results, err := datasource.DefaultDB().Table("u_auth_user").Insert(authUser) + if err != nil { + log.Errorf("Insert err => %v", err) + return results + } + return results +} + +// Insert 批量添加 +func (r *RepoUdmAuthUser) Inserts(authUsers []model.UdmAuthUser) int64 { + var num int64 + n := len(authUsers) + batchSize := 2000 + for i := 0; i < n; i += batchSize { + end := i + batchSize + if end > n { + end = n + } + batch := authUsers[i:end] + + // 调用 InsertMulti 函数将批量数据插入数据库 + results, err := datasource.DefaultDB().Table("u_auth_user").InsertMulti(batch) + if err != nil { + continue + } + num += results + } + return num +} + +// Update 修改更新 +func (r *RepoUdmAuthUser) Update(neID string, authUser model.UdmAuthUser) int64 { + // 查询先 + var user model.UdmAuthUser + has, err := datasource.DefaultDB().Table("u_auth_user").Where("imsi = ? and ne_id = ?", authUser.Imsi, neID).Get(&user) + if !has || err != nil { + return 0 + } + + if authUser.Ki != "" && authUser.Ki != user.Ki { + user.Ki = authUser.Ki + } + if authUser.Amf != "" && authUser.Amf != user.Amf { + user.Amf = authUser.Amf + } + if authUser.AlgoIndex != "" && authUser.AlgoIndex != user.AlgoIndex { + user.AlgoIndex = authUser.AlgoIndex + } + if authUser.Opc != "" && authUser.Opc != user.Opc { + user.Opc = authUser.Opc + } + + results, err := datasource.DefaultDB().Table("u_auth_user").Where("imsi = ? and ne_id = ?", user.Imsi, user.NeID).Update(user) + if err != nil { + return 0 + } + return results +} + +// Delete 删除实体 +func (r *RepoUdmAuthUser) Delete(neID, imsi string) int64 { + results, err := datasource.DefaultDB().Table("u_auth_user").Where("imsi = ? and ne_id = ?", imsi, neID).Delete() + if err != nil { + return results + } + return results +} + +// Delete 删除范围实体 +func (r *RepoUdmAuthUser) Deletes(neID, imsi, num string) int64 { + imsiV, err := strconv.Atoi(imsi) + if err != nil { + return 0 + } + + numV, err := strconv.Atoi(num) + if err != nil { + return 0 + } + + results, err := datasource.DefaultDB().Table("u_auth_user").Where("imsi >= ? and imsi <= ? and ne_id = ?", imsiV, imsiV+numV, neID).Delete() + if err != nil { + return results + } + return results +} diff --git a/features/udm_user/repo/repo_udm_sub_user.go b/features/udm_user/repo/repo_udm_sub_user.go new file mode 100644 index 0000000..202a53a --- /dev/null +++ b/features/udm_user/repo/repo_udm_sub_user.go @@ -0,0 +1,438 @@ +package repo + +import ( + "fmt" + "strconv" + "strings" + + "ems.agt/features/udm_user/model" + "ems.agt/lib/core/datasource" + "ems.agt/lib/core/utils/parse" + "ems.agt/lib/log" +) + +// 实例化数据层 RepoUdmSubUser 结构体 +var NewRepoUdmSubUser = &RepoUdmSubUser{ + selectSql: `select + id, msisdn, imsi, ambr, nssai, rat, arfb, sar, cn, sm_data, smf_sel, eps_dat, ne_id, eps_flag, eps_odb, hplmn_odb, ard, epstpl, context_id, apn_context, static_ip + from u_sub_user`, + + resultMap: map[string]string{ + "id": "ID", + "msisdn": "Msisdn", + "imsi": "Imsi", + "ambr": "Ambr", + "nssai": "Nssai", + "rat": "Rat", + "arfb": "Arfb", + "sar": "Sar", + "cn": "Cn", + "sm_data": "SmData", + "smf_sel": "SmfSel", + "eps_dat": "EpsDat", + "ne_id": "NeID", + "eps_flag": "EpsFlag", + "eps_odb": "EpsOdb", + "hplmn_odb": "HplmnOdb", + "ard": "Ard", + "epstpl": "Epstpl", + "context_id": "ContextId", + "apn_context": "ApnContext", + "static_ip": "StaticIp", + }, +} + +// RepoUdmSubUser UDM签约用户 数据层处理 +type RepoUdmSubUser struct { + // 查询视图对象SQL + selectSql string + // 结果字段与实体映射 + resultMap map[string]string +} + +// convertResultRows 将结果记录转实体结果组 +func (r *RepoUdmSubUser) convertResultRows(rows []map[string]any) []model.UdmSubUser { + arr := make([]model.UdmSubUser, 0) + for _, row := range rows { + UdmUser := model.UdmSubUser{} + for key, value := range row { + if keyMapper, ok := r.resultMap[key]; ok { + datasource.SetFieldValue(&UdmUser, keyMapper, value) + } + } + arr = append(arr, UdmUser) + } + return arr +} + +// SelectPage 根据条件分页查询字典类型 +func (r *RepoUdmSubUser) SelectPage(query map[string]any) map[string]any { + // 查询条件拼接 + var conditions []string + var params []any + if v, ok := query["msisdn"]; ok && v != "" { + conditions = append(conditions, "msisdn like concat(concat('%', ?), '%')") + params = append(params, v) + } + if v, ok := query["imsi"]; ok && v != "" { + conditions = append(conditions, "imsi like concat(concat('%', ?), '%')") + params = append(params, v) + } + if v, ok := query["neId"]; ok && v != "" { + conditions = append(conditions, "ne_id = ?") + params = append(params, v) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + result := map[string]any{ + "total": 0, + "rows": []model.UdmAuthUser{}, + } + + // 查询数量 长度为0直接返回 + totalSql := "select count(1) as 'total' from u_sub_user" + totalRows, err := datasource.RawDB("", totalSql+whereSql, params) + if err != nil { + log.Errorf("total err => %v", err) + return result + } + total := parse.Number(totalRows[0]["total"]) + if total == 0 { + return result + } else { + result["total"] = total + } + + // 分页 + pageNum, pageSize := datasource.PageNumSize(query["pageNum"], query["pageSize"]) + pageSql := " limit ?,? " + params = append(params, pageNum*pageSize) + params = append(params, pageSize) + + // 排序 + sortSql := "" + if v, ok := query["sortField"]; ok && v != "" { + if v == "imsi" { + sortSql += " order by imsi " + } + if v, ok := query["sortOrder"]; ok && v != nil { + if v == "desc" { + sortSql += " desc " + } else { + sortSql += " asc " + } + } + } + + // 查询数据 + querySql := r.selectSql + whereSql + sortSql + pageSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + log.Errorf("query err => %v", err) + return result + } + + // 转换实体 + result["rows"] = r.convertResultRows(results) + return result +} + +// SelectList 根据实体查询 +func (r *RepoUdmSubUser) SelectList(auth model.UdmSubUser) []model.UdmSubUser { + // 查询条件拼接 + var conditions []string + var params []any + if auth.Imsi != "" { + conditions = append(conditions, "imsi = ?") + params = append(params, auth.Imsi) + } + if auth.NeID != "" { + conditions = append(conditions, "ne_id = ?") + params = append(params, auth.NeID) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询数据 + querySql := r.selectSql + whereSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + log.Errorf("query err => %v", err) + } + + // 转换实体 + return r.convertResultRows(results) +} + +// ClearAndInsert 清空ne_id后新增实体 +func (r *RepoUdmSubUser) ClearAndInsert(neID string, subArr []model.UdmSubUser) int64 { + var num int64 = 0 + + // 清空指定ne_id + _, err := datasource.ExecDB("", "TRUNCATE TABLE u_sub_user", nil) + // _, err := datasource.ExecDB("", "DELETE FROM u_sub_user WHERE ne_id = ?", []any{neID}) + if err != nil { + log.Errorf("TRUNCATE err => %v", err) + } + + n := len(subArr) + batchSize := 2000 + for i := 0; i < n; i += batchSize { + end := i + batchSize + if end > n { + end = n + } + batch := subArr[i:end] + + // 调用 InsertMulti 函数将批量数据插入数据库 + results, err := datasource.DefaultDB().Table("u_sub_user").InsertMulti(batch) + if err != nil { + continue + } + num += results + } + + // for _, u := range subArr { + // u.NeID = neID + // results, err := datasource.DefaultDB().Table("u_sub_user").Insert(u) + // if err != nil { + // return num + // } + // num += results + // } + return num +} + +// Insert 新增实体 +func (r *RepoUdmSubUser) Insert(subUser model.UdmSubUser) int64 { + results, err := datasource.DefaultDB().Table("u_sub_user").Insert(subUser) + if err != nil { + return results + } + return results +} + +// Insert 批量添加 +func (r *RepoUdmSubUser) Inserts(subUser []model.UdmSubUser) int64 { + var num int64 + n := len(subUser) + batchSize := 2000 + for i := 0; i < n; i += batchSize { + end := i + batchSize + if end > n { + end = n + } + batch := subUser[i:end] + + // 调用 InsertMulti 函数将批量数据插入数据库 + results, err := datasource.DefaultDB().Table("u_sub_user").InsertMulti(batch) + if err != nil { + continue + } + num += results + } + return num +} + +// Insert4G 批量添加4G用户 +func (r *RepoUdmSubUser) Insert4G(neID string, subUser model.UdmSubUser) int64 { + var insertNum int64 + + imsiV, err := strconv.Atoi(subUser.Imsi) + if err != nil { + return 0 + } + numV, err := strconv.Atoi(subUser.SubNum) + if err != nil { + return 0 + } + + subUser.NeID = neID + for i := 0; i < numV; i++ { + subUser.Imsi = fmt.Sprint(imsiV + i) + + results, err := datasource.DefaultDB().Table("u_sub_user").Insert(subUser) + if err == nil { + insertNum += results + } + } + + return insertNum +} + +// Update 修改更新 +func (r *RepoUdmSubUser) Update(neID string, authUser model.UdmSubUser) int64 { + // 查询先 + var user model.UdmSubUser + has, err := datasource.DefaultDB().Table("u_sub_user").Where("imsi = ? and ne_id = ?", authUser.Imsi, neID).Get(&user) + if !has || err != nil { + return 0 + } + + if authUser.Msisdn != "" && authUser.Msisdn != user.Msisdn { + user.Msisdn = authUser.Msisdn + } + if authUser.Ambr != "" && authUser.Ambr != user.Ambr { + user.Ambr = authUser.Ambr + } + if authUser.Arfb != "" && authUser.Arfb != user.Arfb { + user.Arfb = authUser.Arfb + } + if authUser.Sar != "" && authUser.Sar != user.Sar { + user.Sar = authUser.Sar + } + if authUser.Rat != "" && authUser.Rat != user.Rat { + user.Rat = authUser.Rat + } + if authUser.Cn != "" && authUser.Cn != user.Cn { + user.Cn = authUser.Cn + } + if authUser.SmfSel != "" && authUser.SmfSel != user.SmfSel { + user.SmfSel = authUser.SmfSel + } + if authUser.SmData != "" && authUser.SmData != user.SmData { + user.SmData = authUser.SmData + } + if authUser.EpsDat != "" && authUser.EpsDat != user.EpsDat { + user.EpsDat = authUser.EpsDat + } + if authUser.EpsFlag != "" && authUser.EpsFlag != user.EpsFlag { + user.EpsFlag = authUser.EpsFlag + } + if authUser.EpsOdb != "" && authUser.EpsDat != user.EpsDat { + user.EpsOdb = authUser.EpsOdb + } + if authUser.HplmnOdb != "" && authUser.HplmnOdb != user.HplmnOdb { + user.HplmnOdb = authUser.HplmnOdb + } + if authUser.Epstpl != "" && authUser.Epstpl != user.Epstpl { + user.Epstpl = authUser.Epstpl + } + if authUser.Ard != "" && authUser.Ard != user.Ard { + user.Ard = authUser.Ard + } + if authUser.ContextId != "" && authUser.ContextId != user.ContextId { + user.ContextId = authUser.ContextId + } + if authUser.ApnContext != "" && authUser.ApnContext != user.ApnContext { + user.ApnContext = authUser.ApnContext + } + if authUser.StaticIp != "" && authUser.StaticIp != user.StaticIp { + user.StaticIp = authUser.StaticIp + } + + results, err := datasource.DefaultDB().Table("u_sub_user").Where("imsi = ? and ne_id = ?", user.Imsi, user.NeID).Update(user) + if err != nil { + return 0 + } + return results +} + +// Update4GIP 批量修改4G IP +func (r *RepoUdmSubUser) Update4GIP(neID string, subUser model.UdmSubUser) int64 { + var insertNum int64 + + imsiV, err := strconv.Atoi(subUser.Imsi) + if err != nil || subUser.StaticIp == "" { + return insertNum + } + numV, err := strconv.Atoi(subUser.SubNum) + if err != nil { + return insertNum + } + + for i := 0; i < numV; i++ { + subUser.Imsi = fmt.Sprint(imsiV + i) + + // 查询先 + var user model.UdmSubUser + has, err := datasource.DefaultDB().Table("u_sub_user").Where("imsi = ? and ne_id = ?", subUser.Imsi, neID).Get(&user) + if has && err == nil { + // IP会自动递增 + parts := strings.Split(subUser.StaticIp, ".") + lastPart := parts[3] + lastNum, _ := strconv.Atoi(lastPart) + lastNum += i + newLastPart := strconv.Itoa(lastNum) + parts[3] = newLastPart + newIP := strings.Join(parts, ".") + user.StaticIp = newIP + // 更新 + results, err := datasource.DefaultDB().Table("u_sub_user").Update(user) + if err == nil { + insertNum += results + } + } + } + return insertNum +} + +// UpdateSmData 批量修改sm-data +func (r *RepoUdmSubUser) UpdateSmData(neID string, subUser model.UdmSubUser) int64 { + var insertNum int64 + + imsiV, err := strconv.Atoi(subUser.Imsi) + if err != nil || subUser.StaticIp == "" { + return insertNum + } + numV, err := strconv.Atoi(subUser.SubNum) + if err != nil { + return insertNum + } + + for i := 0; i < numV; i++ { + subUser.Imsi = fmt.Sprint(imsiV + i) + + // 查询先 + var user model.UdmSubUser + has, err := datasource.DefaultDB().Table("u_sub_user").Where("imsi = ? and ne_id = ?", subUser.Imsi, neID).Get(&user) + if has && err == nil { + // IP会自动递增,需提前规划好DNN对应的IP;如dnn不需要绑定IP则不带此字段名 + // parts := strings.Split(subUser.SmData, "&") + user.SmData = subUser.SmData + // 更新 + results, err := datasource.DefaultDB().Table("u_sub_user").Update(user) + if err == nil { + insertNum += results + } + } + } + return insertNum +} + +// Delete 删除实体 +func (r *RepoUdmSubUser) Delete(neID, imsi string) int64 { + results, err := datasource.DefaultDB().Table("u_sub_user").Where("imsi = ? and ne_id = ?", imsi, neID).Delete() + if err != nil { + return results + } + return results +} + +// Delete 删除范围实体 +func (r *RepoUdmSubUser) Deletes(neID, imsi, num string) int64 { + imsiV, err := strconv.Atoi(imsi) + if err != nil { + return 0 + } + + numV, err := strconv.Atoi(num) + if err != nil { + return 0 + } + + results, err := datasource.DefaultDB().Table("u_sub_user").Where("imsi >= ? and imsi <= ? and ne_id = ?", imsiV, imsiV+numV, neID).Delete() + if err != nil { + return results + } + return results +} diff --git a/features/udm_user/service/service_redis_data.go b/features/udm_user/service/service_redis_data.go new file mode 100644 index 0000000..1f208d2 --- /dev/null +++ b/features/udm_user/service/service_redis_data.go @@ -0,0 +1,108 @@ +package service + +import ( + "strings" + + "ems.agt/features/udm_user/model" + "ems.agt/lib/core/redis" +) + +// phoneImsiList 获取所有imsi +// func phoneImsiList() map[string]string { +// phoneAndImsiArr := make(map[string]string, 0) +// phoneKeys, err := redis.GetKeys("udmuser", "1*********") +// if err != nil { +// return phoneAndImsiArr +// } +// for _, phone := range phoneKeys { +// imsi, err := redis.Get("udmuser", phone) +// if err != nil { +// continue +// } +// phoneAndImsiArr[phone] = imsi +// } +// return phoneAndImsiArr +// } + +// redisUdmAuthUserList UDM鉴权用户 +func redisUdmAuthUserList() []model.UdmAuthUser { + user := []model.UdmAuthUser{} + ausfArr, err := redis.GetKeys("udmuser", "ausf:*") + if err != nil { + return user + } + for _, key := range ausfArr { + m, err := redis.GetHash("udmuser", key) + if err != nil { + continue + } + + status := "0" + if _, ok := m["auth_success"]; ok { + status = "1" + } + amf := "" + if v, ok := m["amf"]; ok { + amf = strings.Replace(v, "\r\n", "", 1) + } + a := model.UdmAuthUser{ + Imsi: key[5:], + Amf: amf, + Status: status, + Ki: m["ki"], + AlgoIndex: m["algo"], + Opc: m["opc"], + } + user = append(user, a) + } + return user +} + +// redisUdmSubUserList UDM签约用户 +func redisUdmSubUserList() []model.UdmSubUser { + user := []model.UdmSubUser{} + udmsdArr, err := redis.GetKeys("udmuser", "udm-sd:*") + if err != nil { + return user + } + for _, key := range udmsdArr { + m, err := redis.GetHash("udmuser", key) + if err != nil { + continue + } + + a := model.UdmSubUser{ + Imsi: key[7:], + Msisdn: strings.TrimPrefix(m["gpsi"], "86"), + SmfSel: m["smf-sel"], + SmData: m["sm-dat"], // 1-000001&cmnet&ims&3gnet + } + + // def_ambr,def_nssai,0,def_arfb,def_sar,3,1,12000,1,1000,0,1,- + if v, ok := m["am-dat"]; ok { + arr := strings.Split(v, ",") + a.Ambr = arr[0] + a.Nssai = arr[1] + a.Rat = arr[2] + a.Arfb = arr[3] + a.Sar = arr[4] + a.Cn = arr[5] + } + // 1,64,24,65,def_eps,1,2,010200000000,- + if v, ok := m["eps-dat"]; ok { + a.EpsDat = v + arr := strings.Split(v, ",") + a.EpsFlag = arr[0] + a.EpsOdb = arr[1] + a.HplmnOdb = arr[2] + a.Ard = arr[3] + a.Epstpl = arr[4] + a.ContextId = arr[5] + a.ApnContext = arr[7] + a.StaticIp = arr[8] + } + + user = append(user, a) + } + return user +} diff --git a/features/udm_user/service/service_udm_auth_user.go b/features/udm_user/service/service_udm_auth_user.go new file mode 100644 index 0000000..4fa4b56 --- /dev/null +++ b/features/udm_user/service/service_udm_auth_user.go @@ -0,0 +1,143 @@ +package service + +import ( + "fmt" + "strconv" + + "ems.agt/features/udm_user/model" + "ems.agt/features/udm_user/repo" +) + +// 实例化服务层 ServiceUdmAuthUser 结构体 +var NewServiceUdmAuthUser = &ServiceUdmAuthUser{ + repoAuthUser: *repo.NewRepoUdmAuthUser, +} + +// ServiceUdmAuthUser UDM鉴权用户 服务层处理 +type ServiceUdmAuthUser struct { + repoAuthUser repo.RepoUdmAuthUser +} + +// Save UDM鉴权用户-获取全部保存数据库 +func (r *ServiceUdmAuthUser) Save(neID string) int64 { + var num int64 = 0 + authArr := redisUdmAuthUserList() + // 有数据才清空 + if len(authArr) == 0 { + return num + } + go r.repoAuthUser.ClearAndInsert(neID, authArr) + return int64(len(authArr)) +} + +// Page UDM签约用户-分页查询数据库 +func (r *ServiceUdmAuthUser) Page(query map[string]any) map[string]any { + return r.repoAuthUser.SelectPage(query) +} + +// List UDM签约用户-查询数据库 +func (r *ServiceUdmAuthUser) List(authUser model.UdmAuthUser) []model.UdmAuthUser { + return r.repoAuthUser.SelectList(authUser) +} + +// Insert UDM鉴权用户-新增单个 +// imsi长度15,ki长度32,opc长度0或者32 +func (r *ServiceUdmAuthUser) Insert(neID string, authUser model.UdmAuthUser) int64 { + authUser.NeID = neID + authUser.Status = "1" + return r.repoAuthUser.Insert(authUser) +} + +// Insert UDM鉴权用户-批量添加 +func (r *ServiceUdmAuthUser) Inserts(neID string, authUser model.UdmAuthUser, num string) int64 { + var arr []model.UdmAuthUser + + imsiVlen := len(authUser.Imsi) + imsiV, err := strconv.Atoi(authUser.Imsi) + if err != nil { + return 0 + } + + numV, err := strconv.Atoi(num) + if err != nil { + return 0 + } + + authUser.NeID = neID + authUser.Status = "1" + for i := 0; i < numV; i++ { + imsi := fmt.Sprint(imsiV + i) + if len(imsi) < imsiVlen { + imsi = fmt.Sprintf("%0*s", imsiVlen, imsi) + } + authUser.Imsi = imsi + arr = append(arr, authUser) + } + + return r.repoAuthUser.Inserts(arr) +} + +// InsertCSV UDM鉴权用户-批量添加 +func (r *ServiceUdmAuthUser) InsertCSV(neID string, data []map[string]string) int64 { + var arr []model.UdmAuthUser + for _, v := range data { + var authUser model.UdmAuthUser + authUser.NeID = neID + authUser.Status = "1" + if s, ok := v["imsi"]; ok { + authUser.Imsi = s + } + if s, ok := v["ki"]; ok { + authUser.Ki = s + } + if s, ok := v["amf"]; ok { + authUser.Amf = s + } + if s, ok := v["algo"]; ok { + authUser.AlgoIndex = s + } + if s, ok := v["opc"]; ok { + authUser.Opc = s + } + arr = append(arr, authUser) + } + return r.repoAuthUser.Inserts(arr) +} + +// InsertTxt UDM鉴权用户-批量添加 +func (r *ServiceUdmAuthUser) InsertTxt(neID string, data [][]string) int64 { + var arr []model.UdmAuthUser + for _, v := range data { + if len(v) < 4 { + continue + } + var authUser model.UdmAuthUser + authUser.NeID = neID + authUser.Status = "1" + authUser.Imsi = v[0] + authUser.Ki = v[1] + authUser.AlgoIndex = v[2] + authUser.Amf = v[3] + if len(v) == 5 { + authUser.Opc = v[4] + } + arr = append(arr, authUser) + } + + return r.repoAuthUser.Inserts(arr) +} + +// Insert UDM鉴权用户-修改更新 +func (r *ServiceUdmAuthUser) Update(neID string, authUser model.UdmAuthUser) int64 { + return r.repoAuthUser.Update(neID, authUser) +} + +// Insert UDM鉴权用户-删除单个 +func (r *ServiceUdmAuthUser) Delete(neID, imsi string) int64 { + return r.repoAuthUser.Delete(neID, imsi) +} + +// Insert UDM鉴权用户-删除范围 +func (r *ServiceUdmAuthUser) Deletes(neID, imsi, num string) int64 { + return r.repoAuthUser.Deletes(neID, imsi, num) +} diff --git a/features/udm_user/service/service_udm_sub_user.go b/features/udm_user/service/service_udm_sub_user.go new file mode 100644 index 0000000..6f02244 --- /dev/null +++ b/features/udm_user/service/service_udm_sub_user.go @@ -0,0 +1,201 @@ +package service + +import ( + "fmt" + "strconv" + "strings" + + "ems.agt/features/udm_user/model" + "ems.agt/features/udm_user/repo" +) + +// 实例化服务层 ServiceUdmSubUser 结构体 +var NewServiceUdmSubUser = &ServiceUdmSubUser{ + repoSunUser: *repo.NewRepoUdmSubUser, +} + +// ServiceUdmSubUser UDM签约用户 服务层处理 +type ServiceUdmSubUser struct { + repoSunUser repo.RepoUdmSubUser +} + +// Save UDM签约用户-获取全部保存数据库 +func (r *ServiceUdmSubUser) Save(neID string) int64 { + var num int64 = 0 + subArr := redisUdmSubUserList() + // 有数据才清空 + if len(subArr) == 0 { + return num + } + go r.repoSunUser.ClearAndInsert(neID, subArr) + return int64(len(subArr)) +} + +// Page UDM签约用户-分页查询数据库 +func (r *ServiceUdmSubUser) Page(query map[string]any) map[string]any { + return r.repoSunUser.SelectPage(query) +} + +// List UDM签约用户-查询数据库 +func (r *ServiceUdmSubUser) List(subUser model.UdmSubUser) []model.UdmSubUser { + return r.repoSunUser.SelectList(subUser) +} + +// Insert UDM签约用户-新增单个 +// imsi长度15,ki长度32,opc长度0或者32 +func (r *ServiceUdmSubUser) Insert(neID string, subUser model.UdmSubUser) int64 { + subUser.NeID = neID + return r.repoSunUser.Insert(subUser) +} + +// Insert UDM签约用户-批量添加 +func (r *ServiceUdmSubUser) Inserts(neID string, subUser model.UdmSubUser, num string) int64 { + var arr []model.UdmSubUser + + imsiVlen := len(subUser.Imsi) + imsiV, err := strconv.Atoi(subUser.Imsi) + if err != nil { + return 0 + } + msisdnVlen := len(subUser.Msisdn) + msisdnV, err := strconv.Atoi(subUser.Msisdn) + if err != nil { + return 0 + } + + numV, err := strconv.Atoi(num) + if err != nil { + return 0 + } + + subUser.NeID = neID + for i := 0; i < numV; i++ { + msisdn := fmt.Sprint(msisdnV + i) + if len(msisdn) < msisdnVlen { + msisdn = fmt.Sprintf("%0*s", msisdnVlen, msisdn) + } + subUser.Msisdn = msisdn + + imsi := fmt.Sprint(imsiV + i) + if len(imsi) < imsiVlen { + imsi = fmt.Sprintf("%0*s", imsiVlen, imsi) + } + subUser.Imsi = imsi + + // IP会自动递增 + if subUser.StaticIp != "" { + parts := strings.Split(subUser.StaticIp, ".") + lastPart := parts[3] + lastNum, _ := strconv.Atoi(lastPart) + lastNum += i + newLastPart := strconv.Itoa(lastNum) + parts[3] = newLastPart + newIP := strings.Join(parts, ".") + subUser.StaticIp = newIP + } + arr = append(arr, subUser) + } + + return r.repoSunUser.Inserts(arr) +} + +// InsertCSV UDM签约用户-批量添加 +func (r *ServiceUdmSubUser) InsertCSV(neID string, data []map[string]string) int64 { + var arr []model.UdmSubUser + for _, v := range data { + var subUser model.UdmSubUser + subUser.NeID = neID + if s, ok := v["imsi"]; ok { + subUser.Imsi = s + } + if s, ok := v["msisdn"]; ok { + subUser.Msisdn = s + } + if s, ok := v["ambr"]; ok { + subUser.Ambr = s + } + if s, ok := v["nssai"]; ok { + subUser.Nssai = s + } + if s, ok := v["arfb"]; ok { + subUser.Arfb = s + } + if s, ok := v["sar"]; ok { + subUser.Sar = s + } + if s, ok := v["rat"]; ok { + subUser.Rat = s + } + if s, ok := v["cn"]; ok { + subUser.Cn = s + } + if s, ok := v["smf_sel"]; ok { + subUser.SmfSel = s + } + if s, ok := v["sm_data"]; ok { + subUser.SmData = s + } + if s, ok := v["eps_dat"]; ok { + subUser.EpsDat = s + } + arr = append(arr, subUser) + } + return r.repoSunUser.Inserts(arr) +} + +// InsertTxt UDM签约用户-批量添加 +func (r *ServiceUdmSubUser) InsertTxt(neID string, data [][]string) int64 { + var arr []model.UdmSubUser + for _, v := range data { + if len(v) < 10 { + continue + } + var subUser model.UdmSubUser + subUser.NeID = neID + subUser.Imsi = v[0] + subUser.Msisdn = v[1] + subUser.Ambr = v[2] + subUser.Nssai = v[3] + subUser.Arfb = v[4] + subUser.Sar = v[5] + subUser.Rat = v[6] + subUser.Cn = v[7] + subUser.SmfSel = v[8] + subUser.SmData = v[9] + if len(v) == 11 { + subUser.EpsDat = v[10] + } + arr = append(arr, subUser) + } + return r.repoSunUser.Inserts(arr) +} + +// Insert UDM签约用户-批量添加4G用户 +func (r *ServiceUdmSubUser) Insert4G(neID string, subUser model.UdmSubUser) int64 { + return r.repoSunUser.Insert4G(neID, subUser) +} + +// Insert UDM签约用户-修改更新 +func (r *ServiceUdmSubUser) Update(neID string, subUser model.UdmSubUser) int64 { + return r.repoSunUser.Update(neID, subUser) +} + +// Update4GIP UDM签约用户-批量修改4G IP +func (r *ServiceUdmSubUser) Update4GIP(neID string, subUser model.UdmSubUser) int64 { + return r.repoSunUser.Update4GIP(neID, subUser) +} + +// Update4GIP UDM签约用户-批量修改sm-data +func (r *ServiceUdmSubUser) UpdateSmData(neID string, subUser model.UdmSubUser) int64 { + return r.repoSunUser.UpdateSmData(neID, subUser) +} + +// Insert UDM签约用户-删除单个 +func (r *ServiceUdmSubUser) Delete(neID, imsi string) int64 { + return r.repoSunUser.Delete(neID, imsi) +} + +// Insert UDM签约用户-删除范围 +func (r *ServiceUdmSubUser) Deletes(neID, imsi, num string) int64 { + return r.repoSunUser.Deletes(neID, imsi, num) +} diff --git a/features/ue/ue.go b/features/ue/ue.go new file mode 100644 index 0000000..15013f8 --- /dev/null +++ b/features/ue/ue.go @@ -0,0 +1,272 @@ +package ue + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "ems.agt/lib/dborm" + "ems.agt/lib/global" + "ems.agt/lib/log" + "ems.agt/lib/services" + "ems.agt/restagent/config" + "github.com/go-resty/resty/v2" + "github.com/gorilla/mux" +) + +// AmfNBInfo AMF的NodeB信息 +type AmfNBInfo struct { + ID string `json:"id"` //NodeB ID + Name string `json:"name"` // NodeB name + Address string `json:"address"` // 基站地址 + UENum int `jons:"ueNum"` // UE数量 +} + +// SmfUENum SMF在线用户数 +type SmfUENum struct { + UENum int `json:"ueNum"` // 当前在线用户数 +} + +// SmfUEInfo SMF在线用户信息 +type SmfUEInfo struct { + IMSI string `json:"imsi"` + MSISDN string `json:"msisdn"` + RatType string `json:"ratType"` + PduSessionInfo []struct { + PduSessionID int `json:"pduSessionID"` + IPv4 string `json:"ipv4"` + IPv6 string `json:"ipv6"` + Dnn string `json:"dnn"` + Tai string `json:"tai"` + SstSD string `json:"sstSD"` + UpfN3IP string `json:"upfN3IP"` + RanN3IP string `json:"ranN3IP"` + Activetime string `json:"activeTime"` + } `json:"pduSessionInfo"` +} + +// ImsUEInfo IMS在线用户信息 +type ImsUEInfo struct { + IMSI string `json:"imsi"` + MSISDN string `json:"msisdn"` + IMPU string `json:"impu"` + Barring int `json:"barring"` + RegState int `json:"regState"` + Activetime string `json:"activeTime"` +} + +var ( + UriNBInfo = config.DefaultUriPrefix + "/ueManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/nbInfo" + UriUEInfo = config.DefaultUriPrefix + "/ueManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/ueInfo" + UriUENum = config.DefaultUriPrefix + "/ueManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/ueNum" + + CustomUriNBInfo = config.UriPrefix + "/ueManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/nbInfo" + CustomUriUEInfo = config.UriPrefix + "/ueManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/ueInfo" + CustomUriUENum = config.UriPrefix + "/ueManagement/{apiVersion}/elementType/{elementTypeValue}/objectType/ueNum" +) + +var client = resty.New() + +func init() { + /* + client. + SetTimeout(10 * time.Second). + SetRetryCount(1). + SetRetryWaitTime(1 * time.Second). + SetRetryMaxWaitTime(2 * time.Second). + SetRetryAfter(func(client *resty.Client, resp *resty.Response) (time.Duration, error) { + return 0, errors.New("quota exceeded") + }) + */ + client.SetTimeout(3 * time.Second) +} + +// Get UEInfo from NF/NFs +func GetUEInfoFromNF(w http.ResponseWriter, r *http.Request) { + log.Info("GetUEInfoFromNF processing... ") + + vars := mux.Vars(r) + neType := vars["elementTypeValue"] + if neType == "" { + services.ResponseNotFound404UriNotExist(w, r) + return + } + //neTypeLower := strings.ToLower(neType) + var neId string + neIds := services.GetParamsArrByName("neId", r) + if len(neIds) == 1 { + neId = neIds[0] + } else { + services.ResponseNotFound404UriNotExist(w, r) + return + } + + token, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + log.Debug("token:", token) + + neInfo, err := dborm.XormGetNeInfo(neType, neId) + if err != nil { + log.Error("Failed to XormGetNeInfo:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } else if neInfo == nil { + err := global.ErrCMNotFoundTargetNE + log.Error(global.ErrCMNotFoundTargetNE) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + log.Trace("neInfo:", neInfo) + + hostUri := fmt.Sprintf("http://%s:%v", neInfo.Ip, neInfo.Port) + requestURI2NF := fmt.Sprintf("%s%s", hostUri, r.RequestURI) + + log.Debug("requestURI2NF:", requestURI2NF) + + resp, err := client.R(). + EnableTrace(). + SetHeaders(map[string]string{"accessToken": token}). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + Get(requestURI2NF) + if err != nil { + log.Error("Get system state from NF is failed:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } else { + var response services.DataResponse + _ = json.Unmarshal(resp.Body(), &response) + services.ResponseWithJson(w, resp.StatusCode(), response) + return + } +} + +// Get UEInfo from SMF +func GetUENumFromNF(w http.ResponseWriter, r *http.Request) { + log.Info("GetUENumFromNF processing... ") + + vars := mux.Vars(r) + neType := vars["elementTypeValue"] + if neType == "" { + services.ResponseNotFound404UriNotExist(w, r) + return + } + //neTypeLower := strings.ToLower(neType) + var neId string + neIds := services.GetParamsArrByName("neId", r) + if len(neIds) == 1 { + neId = neIds[0] + } else { + services.ResponseNotFound404UriNotExist(w, r) + return + } + + token, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + log.Debug("token:", token) + + neInfo, err := dborm.XormGetNeInfo(neType, neId) + if err != nil { + log.Error("Failed to XormGetNeInfo:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } else if neInfo == nil { + err := global.ErrCMNotFoundTargetNE + log.Error(global.ErrCMNotFoundTargetNE) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + log.Trace("neInfo:", neInfo) + + hostUri := fmt.Sprintf("http://%s:%v", neInfo.Ip, neInfo.Port) + requestURI2NF := fmt.Sprintf("%s%s", hostUri, r.RequestURI) + + log.Debug("requestURI2NF:", requestURI2NF) + + resp, err := client.R(). + EnableTrace(). + SetHeaders(map[string]string{"accessToken": token}). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + Get(requestURI2NF) + if err != nil { + log.Error("Get system state from NF is failed:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } else { + var response services.DataResponse + _ = json.Unmarshal(resp.Body(), &response) + services.ResponseWithJson(w, resp.StatusCode(), response) + return + } +} + +// Get UEInfo from NF/NFs +func GetNBInfoFromNF(w http.ResponseWriter, r *http.Request) { + log.Info("GetNBInfoFromNF processing... ") + + vars := mux.Vars(r) + neType := vars["elementTypeValue"] + if neType == "" { + services.ResponseNotFound404UriNotExist(w, r) + return + } + //neTypeLower := strings.ToLower(neType) + var neId string + neIds := services.GetParamsArrByName("neId", r) + if len(neIds) == 1 { + neId = neIds[0] + } else { + services.ResponseNotFound404UriNotExist(w, r) + return + } + + token, err := services.CheckFrontValidRequest(w, r) + if err != nil { + log.Error("Request error:", err) + return + } + log.Debug("token:", token) + + neInfo, err := dborm.XormGetNeInfo(neType, neId) + if err != nil { + log.Error("Failed to XormGetNeInfo:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } else if neInfo == nil { + err := global.ErrCMNotFoundTargetNE + log.Error(global.ErrCMNotFoundTargetNE) + services.ResponseInternalServerError500ProcessError(w, err) + return + } + log.Trace("neInfo:", neInfo) + + hostUri := fmt.Sprintf("http://%s:%v", neInfo.Ip, neInfo.Port) + requestURI2NF := fmt.Sprintf("%s%s", hostUri, r.RequestURI) + + log.Debug("requestURI2NF:", requestURI2NF) + + resp, err := client.R(). + EnableTrace(). + SetHeaders(map[string]string{"accessToken": token}). + SetHeaders(map[string]string{"User-Agent": config.GetDefaultUserAgent()}). + SetHeaders(map[string]string{"Content-Type": "application/json;charset=UTF-8"}). + Get(requestURI2NF) + if err != nil { + log.Error("Get system state from NF is failed:", err) + services.ResponseInternalServerError500ProcessError(w, err) + return + } else { + var response services.DataResponse + _ = json.Unmarshal(resp.Body(), &response) + services.ResponseWithJson(w, resp.StatusCode(), response) + return + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..130fb00 --- /dev/null +++ b/go.mod @@ -0,0 +1,75 @@ +module ems.agt + +go 1.20 + +require ( + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/go-admin-team/go-admin-core v1.3.12-0.20221121065133-27b7dbe27a8f + github.com/go-resty/resty/v2 v2.7.0 + github.com/go-sql-driver/mysql v1.7.1 + github.com/gorilla/mux v1.8.0 + github.com/gorilla/websocket v1.5.0 + github.com/gosnmp/gosnmp v1.35.0 + github.com/lestrrat/go-file-rotatelogs v0.0.0-20180223000712-d3151e2a480f + github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/redis/go-redis/v9 v9.1.0 + github.com/robfig/cron/v3 v3.0.1 + github.com/shirou/gopsutil v3.21.11+incompatible + github.com/shirou/gopsutil/v3 v3.23.7 + github.com/spf13/afero v1.9.5 + github.com/spf13/viper v1.16.0 + golang.org/x/crypto v0.12.0 + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df + gopkg.in/yaml.v3 v3.0.1 + xorm.io/xorm v1.3.2 +) + +require ( + github.com/mattn/go-sqlite3 v1.14.15 // indirect + github.com/onsi/ginkgo v1.16.5 // indirect + github.com/onsi/gomega v1.21.1 // indirect +) + +require ( + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/lestrrat/go-envload v0.0.0-20180220120943-6ed08b54a570 // indirect + github.com/lestrrat/go-strftime v0.0.0-20180220042222-ba3bf9c1d042 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mojocn/base64Captcha v1.3.5 + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/spf13/cast v1.5.1 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.4.2 // indirect + github.com/syndtr/goleveldb v1.0.0 // indirect + github.com/tebeka/strftime v0.1.5 // indirect + github.com/tklauser/go-sysconf v0.3.11 // indirect + github.com/tklauser/numcpus v0.6.0 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect + golang.org/x/image v0.5.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.11.0 // indirect + golang.org/x/text v0.12.0 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a962ae4 --- /dev/null +++ b/go.sum @@ -0,0 +1,1101 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= +gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= +gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bsm/ginkgo/v2 v2.9.5 h1:rtVBYPs3+TC5iLUVOis1B9tjLTup7Cj5IfzosKtvTJ0= +github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 h1:Ghm4eQYC0nEPnSJdVkTrXpu9KtoVCSo1hg7mtI7G9KU= +github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239/go.mod h1:Gdwt2ce0yfBxPvZrHkprdPPTTS3N5rwmLE8T22KBXlw= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-admin-team/go-admin-core v1.3.12-0.20221121065133-27b7dbe27a8f h1:2xHpluWqY/ZlYoUpOU8VwDponYSnukRDhkOr7rk3ffU= +github.com/go-admin-team/go-admin-core v1.3.12-0.20221121065133-27b7dbe27a8f/go.mod h1:a9/XW1rCChPLVJ3bST13hB6R8YfVjYeF0GYjb8If6Yg= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= +github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gosnmp/gosnmp v1.35.0 h1:EuWWNPxTCdAUx2/NbQcSa3WdNxjzpy4Phv57b4MWpJM= +github.com/gosnmp/gosnmp v1.35.0/go.mod h1:2AvKZ3n9aEl5TJEo/fFmf/FGO4Nj4cVeEc5yuk88CYc= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk= +github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= +github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.8.1/go.mod h1:JV6m6b6jhjdmzchES0drzCcYcAHS1OPD5xu3OZ/lE2g= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0= +github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= +github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= +github.com/jackc/pgtype v1.7.0/go.mod h1:ZnHF+rMePVqDKaOfJVI4Q8IVvAQMryDlDkZnKOI75BE= +github.com/jackc/pgtype v1.8.0/go.mod h1:PqDKcEBtllAtk/2p6z6SHdXW5UB+MhE75tUol2OKexE= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA= +github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= +github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= +github.com/jackc/pgx/v4 v4.11.0/go.mod h1:i62xJgdrtVDsnL3U8ekyrQXEwGNTRoG7/8r+CIdYfcc= +github.com/jackc/pgx/v4 v4.12.0/go.mod h1:fE547h6VulLPA3kySjfnSG/e2D861g/50JlVUa/ub60= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 h1:IPJ3dvxmJ4uczJe5YQdrYB16oTJlGSC/OyZDqUk9xX4= +github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869/go.mod h1:cJ6Cj7dQo+O6GJNiMx+Pa94qKj+TG8ONdKHgMNIyyag= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/lestrrat/go-envload v0.0.0-20180220120943-6ed08b54a570 h1:0iQektZGS248WXmGIYOwRXSQhD4qn3icjMpuxwO7qlo= +github.com/lestrrat/go-envload v0.0.0-20180220120943-6ed08b54a570/go.mod h1:BLt8L9ld7wVsvEWQbuLrUZnCMnUmLZ+CGDzKtclrTlE= +github.com/lestrrat/go-file-rotatelogs v0.0.0-20180223000712-d3151e2a480f h1:sgUSP4zdTUZYZgAGGtN5Lxk92rK+JUFOwf+FT99EEI4= +github.com/lestrrat/go-file-rotatelogs v0.0.0-20180223000712-d3151e2a480f/go.mod h1:UGmTpUd3rjbtfIpwAPrcfmGf/Z1HS95TATB+m57TPB8= +github.com/lestrrat/go-strftime v0.0.0-20180220042222-ba3bf9c1d042 h1:Bvq8AziQ5jFF4BHGAEDSqwPW1NJS3XshxbRCxtjFAZc= +github.com/lestrrat/go-strftime v0.0.0-20180220042222-ba3bf9c1d042/go.mod h1:TPpsiPUEh0zFL1Snz4crhMlBe60PYxRHr5oFF3rRYg0= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mojocn/base64Captcha v1.3.5 h1:Qeilr7Ta6eDtG4S+tQuZ5+hO+QHbiGAJdi4PfoagaA0= +github.com/mojocn/base64Captcha v1.3.5/go.mod h1:/tTTXn4WTpX9CfrmipqRytCpJ27Uw3G6I7NcP2WwcmY= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.21.1 h1:OB/euWYIExnPBohllTicTHmGTrMaqJ67nIu80j0/uEM= +github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/redis/go-redis/v9 v9.1.0 h1:137FnGdk+EQdCbye1FW+qOEcY5S+SpY9T0NiuqvtfMY= +github.com/redis/go-redis/v9 v9.1.0/go.mod h1:urWj3He21Dj5k4TK1y59xH8Uj6ATueP8AH1cY3lZl4c= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shirou/gopsutil/v3 v3.23.7 h1:C+fHO8hfIppoJ1WdsVm1RoI0RwXoNdfTK7yWXV0wVj4= +github.com/shirou/gopsutil/v3 v3.23.7/go.mod h1:c4gnmoRC0hQuaLqvxnx1//VXQ0Ms/X9UnJF8pddY5z4= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= +github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= +github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= +github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= +github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +github.com/tebeka/strftime v0.1.5 h1:1NQKN1NiQgkqd/2moD6ySP/5CoZQsKa1d3ZhJ44Jpmg= +github.com/tebeka/strftime v0.1.5/go.mod h1:29/OidkoWHdEKZqzyDLUyC+LmgDgdHo4WAFCDT7D/Ig= +github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= +github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= +github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= +github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190501045829-6d32002ffd75/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI= +golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.18 h1:rMZhRcWrba0y3nVmdiQ7kxAgOOSq2m2f2VzjHLgEs6U= +modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60= +modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw= +modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI= +modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag= +modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw= +modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ= +modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c= +modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo= +modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg= +modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I= +modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs= +modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8= +modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE= +modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk= +modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w= +modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE= +modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8= +modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc= +modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU= +modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE= +modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk= +modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI= +modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE= +modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg= +modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74= +modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU= +modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU= +modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc= +modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM= +modernc.org/ccgo/v3 v3.12.65/go.mod h1:D6hQtKxPNZiY6wDBtehSGKFKmyXn53F8nGTpH+POmS4= +modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ= +modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84= +modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ= +modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY= +modernc.org/ccgo/v3 v3.12.82 h1:wudcnJyjLj1aQQCXF3IM9Gz2X6UNjw+afIghzdtn0v8= +modernc.org/ccgo/v3 v3.12.82/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w= +modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= +modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q= +modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg= +modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M= +modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU= +modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE= +modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso= +modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8= +modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8= +modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I= +modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk= +modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY= +modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE= +modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg= +modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM= +modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg= +modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo= +modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8= +modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ= +modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA= +modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM= +modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg= +modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE= +modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM= +modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU= +modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw= +modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M= +modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18= +modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8= +modernc.org/libc v1.11.70/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw= +modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw= +modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0= +modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI= +modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE= +modernc.org/libc v1.11.87 h1:PzIzOqtlzMDDcCzJ5cUP6h/Ku6Fa9iyflP2ccTY64aE= +modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY= +modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc= +modernc.org/memory v1.0.5 h1:XRch8trV7GgvTec2i7jc33YlUI0RKVDBvZ5eZ5m8y14= +modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM= +modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.14.2 h1:ohsW2+e+Qe2To1W6GNezzKGwjXwSax6R+CrhRxVaFbE= +modernc.org/sqlite v1.14.2/go.mod h1:yqfn85u8wVOE6ub5UT8VI9JjhrwBUUCNyTACN0h6Sx8= +modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs= +modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/tcl v1.8.13/go.mod h1:V+q/Ef0IJaNUSECieLU4o+8IScapxnMyFV6i/7uQlAY= +modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk= +modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.2.19/go.mod h1:+ZpP0pc4zz97eukOzW3xagV/lS82IpPN9NGG5pNF9vY= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= +xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978 h1:bvLlAPW1ZMTWA32LuZMBEGHAUOcATZjzHcotf3SWweM= +xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= +xorm.io/xorm v1.3.2 h1:uTRRKF2jYzbZ5nsofXVUx6ncMaek+SHjWYtCXyZo1oM= +xorm.io/xorm v1.3.2/go.mod h1:9NbjqdnjX6eyjRRhh01GHm64r6N9shTb/8Ak3YRt8Nw= diff --git a/lib/aes/aes.go b/lib/aes/aes.go new file mode 100644 index 0000000..5ce2fb4 --- /dev/null +++ b/lib/aes/aes.go @@ -0,0 +1,64 @@ +package aes + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "encoding/base64" +) + +func AesEncrypt(orig string, key string) string { + // 转成字节数组 + origData := []byte(orig) + k := []byte(key) + + // 分组秘钥 + block, _ := aes.NewCipher(k) + // 获取秘钥块的长度 + blockSize := block.BlockSize() + // 补全码 + origData = PKCS7Padding(origData, blockSize) + // 加密模式 + blockMode := cipher.NewCBCEncrypter(block, k[:blockSize]) + // 创建数组 + cryted := make([]byte, len(origData)) + // 加密 + blockMode.CryptBlocks(cryted, origData) + + return base64.StdEncoding.EncodeToString(cryted) + +} + +func AesDecrypt(cryted string, key string) string { + // 转成字节数组 + crytedByte, _ := base64.StdEncoding.DecodeString(cryted) + k := []byte(key) + + // 分组秘钥 + block, _ := aes.NewCipher(k) + // 获取秘钥块的长度 + blockSize := block.BlockSize() + // 加密模式 + blockMode := cipher.NewCBCDecrypter(block, k[:blockSize]) + // 创建数组 + orig := make([]byte, len(crytedByte)) + // 解密 + blockMode.CryptBlocks(orig, crytedByte) + // 去补全码 + orig = PKCS7UnPadding(orig) + return string(orig) +} + +// 补码 +func PKCS7Padding(ciphertext []byte, blocksize int) []byte { + padding := blocksize - len(ciphertext)%blocksize + padtext := bytes.Repeat([]byte{byte(padding)}, padding) + return append(ciphertext, padtext...) +} + +// 去码 +func PKCS7UnPadding(origData []byte) []byte { + length := len(origData) + unpadding := int(origData[length-1]) + return origData[:(length - unpadding)] +} diff --git a/lib/core/account/account.go b/lib/core/account/account.go new file mode 100644 index 0000000..2279141 --- /dev/null +++ b/lib/core/account/account.go @@ -0,0 +1,54 @@ +package account + +import ( + "fmt" + "strconv" + "time" + + sysMenuService "ems.agt/features/sys_menu/service" + sysRoleService "ems.agt/features/sys_role/service" + "ems.agt/lib/core/cache" + "ems.agt/lib/core/conf" + "ems.agt/lib/core/vo" + "ems.agt/lib/dborm" +) + +// 登录缓存用户信息 +func CacheLoginUser(user *dborm.User) { + // 过期时间 + expiresStr, err := dborm.XormGetConfigValue("Security", "sessionExpires") + if err != nil { + expiresStr = "18000" + } + expiresValue, _ := strconv.Atoi(expiresStr) + expireTime := time.Duration(expiresValue) * time.Second + + nowTime := time.Now().UnixMilli() + + // 登录用户 + loginUser := vo.LoginUser{ + UserID: fmt.Sprint(user.Id), + UserName: user.Name, + ExpireTime: nowTime + expireTime.Milliseconds(), + LoginTime: nowTime, + User: *user, + } + + // 是否管理员 + if conf.IsAdmin(loginUser.UserID) { + loginUser.Permissions = []string{"*:*:*"} + } else { + // 获取权限标识 + loginUser.Permissions = sysMenuService.NewRepoSysMenu.SelectMenuPermsByUserId(loginUser.UserID) + // 获取角色信息 + loginUser.User.Roles = sysRoleService.NewRepoSysRole.SelectRoleListByUserId(loginUser.UserID) + } + + // 缓存时间 + cache.SetLocalTTL(user.AccountId, loginUser, time.Duration(expireTime)) +} + +// 清除缓存用户信息 +func ClearLoginUser(accountId string) { + cache.DeleteLocalTTL(accountId) +} diff --git a/lib/core/cache/lcoal.go b/lib/core/cache/lcoal.go new file mode 100644 index 0000000..f6f12af --- /dev/null +++ b/lib/core/cache/lcoal.go @@ -0,0 +1,56 @@ +package cache + +import ( + "strings" + "time" + + "github.com/patrickmn/go-cache" +) + +// 创建一个全局的不过期缓存对象 +var cNoExpiration = cache.New(cache.NoExpiration, cache.NoExpiration) + +// 将数据放入缓存,并设置永不过期 +func SetLocal(key string, value any) { + cNoExpiration.Set(key, value, cache.NoExpiration) +} + +// 从缓存中获取数据 +func GetLocal(key string) (any, bool) { + return cNoExpiration.Get(key) +} + +// 从缓存中删除数据 +func DeleteLocal(key string) { + cNoExpiration.Delete(key) +} + +// 获取指定前缀的所有键 +func GetLocalKeys(prefix string) []string { + prefix = strings.TrimSuffix(prefix, "*") + var keys []string + for key := range cNoExpiration.Items() { + if strings.HasPrefix(key, prefix) { + keys = append(keys, key) + } + } + return keys +} + +// 创建一个全局的过期缓存对象 +var cTTL = cache.New(6*time.Hour, 12*time.Hour) + +// 设置具有过期时间的缓存项 +func SetLocalTTL(key string, value any, expiration time.Duration) { + cTTL.Set(key, value, expiration) +} + +// 从缓存中获取数据 +func GetLocalTTL(key string) (any, bool) { + return cTTL.Get(key) +} + +// 从缓存中删除数据 +func DeleteLocalTTL(key string) { + cTTL.Delete(key) +} diff --git a/lib/core/cmd/cmd.go b/lib/core/cmd/cmd.go new file mode 100644 index 0000000..085494d --- /dev/null +++ b/lib/core/cmd/cmd.go @@ -0,0 +1,201 @@ +package cmd + +import ( + "bytes" + "context" + "fmt" + "os/exec" + "strings" + "time" +) + +func Exec(cmdStr string) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + cmd := exec.Command("bash", "-c", cmdStr) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + if ctx.Err() == context.DeadlineExceeded { + return "", fmt.Errorf("errCmdTimeout %v", err) + } + if err != nil { + errMsg := "" + if len(stderr.String()) != 0 { + errMsg = fmt.Sprintf("stderr: %s", stderr.String()) + } + if len(stdout.String()) != 0 { + if len(errMsg) != 0 { + errMsg = fmt.Sprintf("%s; stdout: %s", errMsg, stdout.String()) + } else { + errMsg = fmt.Sprintf("stdout: %s", stdout.String()) + } + } + return errMsg, err + } + return stdout.String(), nil +} + +func ExecWithTimeOut(cmdStr string, timeout time.Duration) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + cmd := exec.Command("bash", "-c", cmdStr) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + if ctx.Err() == context.DeadlineExceeded { + return "", fmt.Errorf("errCmdTimeout %v", err) + } + if err != nil { + errMsg := "" + if len(stderr.String()) != 0 { + errMsg = fmt.Sprintf("stderr: %s", stderr.String()) + } + if len(stdout.String()) != 0 { + if len(errMsg) != 0 { + errMsg = fmt.Sprintf("%s; stdout: %s", errMsg, stdout.String()) + } else { + errMsg = fmt.Sprintf("stdout: %s", stdout.String()) + } + } + return errMsg, err + } + return stdout.String(), nil +} + +func ExecCronjobWithTimeOut(cmdStr string, workdir string, timeout time.Duration) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + cmd := exec.Command("bash", "-c", cmdStr) + cmd.Dir = workdir + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + if ctx.Err() == context.DeadlineExceeded { + return "", fmt.Errorf("errCmdTimeout %v", err) + } + + errMsg := "" + if len(stderr.String()) != 0 { + errMsg = fmt.Sprintf("stderr:\n %s", stderr.String()) + } + if len(stdout.String()) != 0 { + if len(errMsg) != 0 { + errMsg = fmt.Sprintf("%s \n\n; stdout:\n %s", errMsg, stdout.String()) + } else { + errMsg = fmt.Sprintf("stdout:\n %s", stdout.String()) + } + } + return errMsg, err +} + +func Execf(cmdStr string, a ...interface{}) (string, error) { + cmd := exec.Command("bash", "-c", fmt.Sprintf(cmdStr, a...)) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + if err != nil { + errMsg := "" + if len(stderr.String()) != 0 { + errMsg = fmt.Sprintf("stderr: %s", stderr.String()) + } + if len(stdout.String()) != 0 { + if len(errMsg) != 0 { + errMsg = fmt.Sprintf("%s; stdout: %s", errMsg, stdout.String()) + } else { + errMsg = fmt.Sprintf("stdout: %s", stdout.String()) + } + } + return errMsg, err + } + return stdout.String(), nil +} + +func ExecWithCheck(name string, a ...string) (string, error) { + cmd := exec.Command(name, a...) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + if err != nil { + errMsg := "" + if len(stderr.String()) != 0 { + errMsg = fmt.Sprintf("stderr: %s", stderr.String()) + } + if len(stdout.String()) != 0 { + if len(errMsg) != 0 { + errMsg = fmt.Sprintf("%s; stdout: %s", errMsg, stdout.String()) + } else { + errMsg = fmt.Sprintf("stdout: %s", stdout.String()) + } + } + return errMsg, err + } + return stdout.String(), nil +} + +func ExecScript(scriptPath, workDir string) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + cmd := exec.Command("bash", scriptPath) + cmd.Dir = workDir + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + if ctx.Err() == context.DeadlineExceeded { + return "", fmt.Errorf("errCmdTimeout %v", err) + } + if err != nil { + errMsg := "" + if len(stderr.String()) != 0 { + errMsg = fmt.Sprintf("stderr: %s", stderr.String()) + } + if len(stdout.String()) != 0 { + if len(errMsg) != 0 { + errMsg = fmt.Sprintf("%s; stdout: %s", errMsg, stdout.String()) + } else { + errMsg = fmt.Sprintf("stdout: %s", stdout.String()) + } + } + return errMsg, err + } + return stdout.String(), nil +} + +func CheckIllegal(args ...string) bool { + if args == nil { + return false + } + for _, arg := range args { + if strings.Contains(arg, "&") || strings.Contains(arg, "|") || strings.Contains(arg, ";") || + strings.Contains(arg, "$") || strings.Contains(arg, "'") || strings.Contains(arg, "`") || + strings.Contains(arg, "(") || strings.Contains(arg, ")") || strings.Contains(arg, "\"") { + return true + } + } + return false +} + +func HasNoPasswordSudo() bool { + cmd2 := exec.Command("sudo", "-n", "ls") + err2 := cmd2.Run() + return err2 == nil +} + +func SudoHandleCmd() string { + cmd := exec.Command("sudo", "-n", "ls") + if err := cmd.Run(); err == nil { + return "sudo " + } + return "" +} + +func Which(name string) bool { + _, err := exec.LookPath(name) + return err == nil +} diff --git a/lib/core/conf/conf.go b/lib/core/conf/conf.go new file mode 100644 index 0000000..506c66a --- /dev/null +++ b/lib/core/conf/conf.go @@ -0,0 +1,52 @@ +package conf + +import ( + "fmt" + "time" + + "github.com/spf13/viper" +) + +// 配置文件读取 +func InitConfig(configFile string) { + // 设置配置文件路径 + viper.SetConfigFile(configFile) + + // 读取配置文件 + err := viper.ReadInConfig() + if err != nil { + fmt.Printf("读取配置文件失败: %v \n", err) + return + } + + // 记录程序开始运行的时间点 + viper.Set("runTime", time.Now()) +} + +// RunTime 程序开始运行的时间 +func RunTime() time.Time { + return viper.GetTime("runTime") +} + +// Get 获取配置信息 +// +// Get("framework.name") +func Get(key string) any { + return viper.Get(key) +} + +// IsAdmin 用户是否为管理员 +func IsAdmin(userID string) bool { + if userID == "" { + return false + } + // 从本地配置获取user信息 + // admins := Get("user.adminList").([]any) + admins := []string{"1", "2", "3"} + for _, s := range admins { + if s == userID { + return true + } + } + return false +} diff --git a/lib/core/constants/cachekey/cachekey.go b/lib/core/constants/cachekey/cachekey.go new file mode 100644 index 0000000..0e69445 --- /dev/null +++ b/lib/core/constants/cachekey/cachekey.go @@ -0,0 +1,24 @@ +package cachekey + +// 缓存的key常量 + +// 登录用户 +const LOGIN_TOKEN_KEY = "login_tokens:" + +// 验证码 +const CAPTCHA_CODE_KEY = "captcha_codes:" + +// 参数管理 +const SYS_CONFIG_KEY = "sys_config:" + +// 字典管理 +const SYS_DICT_KEY = "sys_dict:" + +// 防重提交 +const REPEAT_SUBMIT_KEY = "repeat_submit:" + +// 限流 +const RATE_LIMIT_KEY = "rate_limit:" + +// 登录账户密码错误次数 +const PWD_ERR_CNT_KEY = "pwd_err_cnt:" diff --git a/lib/core/datasource/datasource.go b/lib/core/datasource/datasource.go new file mode 100644 index 0000000..cde0a71 --- /dev/null +++ b/lib/core/datasource/datasource.go @@ -0,0 +1,49 @@ +package datasource + +import ( + "database/sql" + "regexp" + + "ems.agt/lib/dborm" + "xorm.io/xorm" +) + +// 获取默认数据源 +func DefaultDB() *xorm.Engine { + return dborm.DbClient.XEngine +} + +// RawDB 原生查询语句 +func RawDB(source string, sql string, parameters []any) ([]map[string]any, error) { + // 数据源 + db := DefaultDB() + + // 使用正则表达式替换连续的空白字符为单个空格 + fmtSql := regexp.MustCompile(`\s+`).ReplaceAllString(sql, " ") + + // log.Infof("sql=> %v", fmtSql) + // log.Infof("parameters=> %v", parameters) + + // 查询结果 + var rows []map[string]any + err := db.SQL(fmtSql, parameters...).Find(&rows) + if err != nil { + return nil, err + } + return rows, nil +} + +// ExecDB 原生执行语句 +func ExecDB(source string, sql string, parameters []any) (sql.Result, error) { + // 数据源 + db := DefaultDB() + + // 使用正则表达式替换连续的空白字符为单个空格 + fmtSql := regexp.MustCompile(`\s+`).ReplaceAllString(sql, " ") + // 执行结果 + res, err := db.Exec(append([]any{fmtSql}, parameters...)...) + if err != nil { + return nil, err + } + return res, err +} diff --git a/lib/core/datasource/repo.go b/lib/core/datasource/repo.go new file mode 100644 index 0000000..71bc422 --- /dev/null +++ b/lib/core/datasource/repo.go @@ -0,0 +1,135 @@ +package datasource + +import ( + "fmt" + "reflect" + "strconv" + "strings" +) + +// PageNumSize 分页页码记录数 +func PageNumSize(pageNum, pageSize any) (int, int) { + // 记录起始索引 + pageNumStr := fmt.Sprintf("%v", pageNum) + num := 1 + if v, err := strconv.Atoi(pageNumStr); err == nil && v > 0 { + if num > 5000 { + num = 5000 + } + num = v + } + + // 显示记录数 + pageSizeStr := fmt.Sprintf("%v", pageSize) + size := 10 + if v, err := strconv.Atoi(pageSizeStr); err == nil && v > 0 { + if size < 0 { + size = 10 + } else if size > 1000 { + size = 1000 + } else { + size = v + } + } + return num - 1, size +} + +// SetFieldValue 判断结构体内是否存在指定字段并设置值 +func SetFieldValue(obj any, fieldName string, value any) { + // 获取结构体的反射值 + userValue := reflect.ValueOf(obj) + + // 获取字段的反射值 + fieldValue := userValue.Elem().FieldByName(fieldName) + + // 检查字段是否存在 + if fieldValue.IsValid() && fieldValue.CanSet() { + // 获取字段的类型 + fieldType := fieldValue.Type() + + // 转换传入的值类型为字段类型 + switch fieldType.Kind() { + case reflect.String: + if value == nil { + fieldValue.SetString("") + } else { + fieldValue.SetString(fmt.Sprintf("%v", value)) + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + intValue, err := strconv.ParseInt(fmt.Sprintf("%v", value), 10, 64) + if err != nil { + intValue = 0 + } + fieldValue.SetInt(intValue) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + uintValue, err := strconv.ParseUint(fmt.Sprintf("%v", value), 10, 64) + if err != nil { + uintValue = 0 + } + fieldValue.SetUint(uintValue) + case reflect.Float32, reflect.Float64: + floatValue, err := strconv.ParseFloat(fmt.Sprintf("%v", value), 64) + if err != nil { + floatValue = 0 + } + fieldValue.SetFloat(floatValue) + default: + // 设置字段的值 + fieldValue.Set(reflect.ValueOf(value).Convert(fieldValue.Type())) + } + } +} + +// ConvertIdsSlice 将 []string 转换为 []any +func ConvertIdsSlice(ids []string) []any { + // 将 []string 转换为 []any + arr := make([]any, len(ids)) + for i, v := range ids { + arr[i] = v + } + return arr +} + +// 查询-参数值的占位符 +func KeyPlaceholderByQuery(sum int) string { + placeholders := make([]string, sum) + for i := 0; i < sum; i++ { + placeholders[i] = "?" + } + return strings.Join(placeholders, ",") +} + +// 插入-参数映射键值占位符 keys, placeholder, values +func KeyPlaceholderValueByInsert(params map[string]any) ([]string, string, []any) { + // 参数映射的键 + keys := make([]string, len(params)) + // 参数映射的值 + values := make([]any, len(params)) + sum := 0 + for k, v := range params { + keys[sum] = k + values[sum] = v + sum++ + } + // 参数值的占位符 + placeholders := make([]string, sum) + for i := 0; i < sum; i++ { + placeholders[i] = "?" + } + return keys, strings.Join(placeholders, ","), values +} + +// 更新-参数映射键值占位符 keys, values +func KeyValueByUpdate(params map[string]any) ([]string, []any) { + // 参数映射的键 + keys := make([]string, len(params)) + // 参数映射的值 + values := make([]any, len(params)) + sum := 0 + for k, v := range params { + keys[sum] = k + "=?" + values[sum] = v + sum++ + } + return keys, values +} diff --git a/lib/core/file/csv.go b/lib/core/file/csv.go new file mode 100644 index 0000000..1473a79 --- /dev/null +++ b/lib/core/file/csv.go @@ -0,0 +1,88 @@ +package file + +import ( + "encoding/csv" + "os" + "path/filepath" + "strings" + + "ems.agt/lib/log" +) + +// 写入CSV文件,需要转换数据 +// 例如: +// data := [][]string{} +// data = append(data, []string{"姓名", "年龄", "城市"}) +// data = append(data, []string{"1", "2", "3"}) +// err := file.WriterCSVFile(data, filePath) +func WriterCSVFile(data [][]string, filePath string) error { + // 获取文件所在的目录路径 + dirPath := filepath.Dir(filePath) + + // 确保文件夹路径存在 + err := os.MkdirAll(dirPath, os.ModePerm) + if err != nil { + log.Errorf("创建文件夹失败 CreateFile %v", err) + } + + // 创建或打开文件 + file, err := os.Create(filePath) + if err != nil { + return err + } + defer file.Close() + + // 创建CSV编写器 + writer := csv.NewWriter(file) + defer writer.Flush() + + // 写入数据 + for _, row := range data { + writer.Write(row) + } + return nil +} + +// 读取CSV文件,转换map数据 +func ReadCSVFile(filePath string) []map[string]string { + // 创建 map 存储 CSV 数据 + arr := make([]map[string]string, 0) + + // 打开 CSV 文件 + file, err := os.Open(filePath) + if err != nil { + log.Fatal("无法打开 CSV 文件:", err) + return arr + } + defer file.Close() + + // 创建 CSV Reader + reader := csv.NewReader(file) + + // 读取 CSV 头部行 + header, err := reader.Read() + if err != nil { + log.Fatal("无法读取 CSV 头部行:", err) + return arr + } + + // 遍历 CSV 数据行 + for { + // 读取一行数据 + record, err := reader.Read() + if err != nil { + // 到达文件末尾或遇到错误时退出循环 + break + } + + // 将 CSV 数据插入到 map 中 + data := make(map[string]string) + for i, value := range record { + key := strings.ToLower(header[i]) + data[key] = value + } + arr = append(arr, data) + } + + return arr +} diff --git a/lib/core/file/ssh.go b/lib/core/file/ssh.go new file mode 100644 index 0000000..c33a717 --- /dev/null +++ b/lib/core/file/ssh.go @@ -0,0 +1,49 @@ +package file + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + + "ems.agt/lib/core/conf" + "ems.agt/lib/log" +) + +// 网元NE 文件复制到远程文件 +func FileSCPLocalToNe(neIp, localPath, nePath string) error { + usernameNe := conf.Get("ne.user").(string) + // scp /path/to/local/file.txt user@remote-server:/path/to/remote/directory/ + neDir := fmt.Sprintf("%s@%s:%s", usernameNe, neIp, nePath) + cmd := exec.Command("scp", "-r", localPath, neDir) + out, err := cmd.CombinedOutput() + if err != nil { + return err + } + log.Infof("FileSCPLocalToNe %s", string(out)) + return nil +} + +// 网元NE 远程文件复制到本地文件 +func FileSCPNeToLocal(neIp, nePath, localPath string) error { + // 获取文件所在的目录路径 + dirPath := filepath.Dir(localPath) + + // 确保文件夹路径存在 + err := os.MkdirAll(dirPath, os.ModePerm) + if err != nil { + log.Errorf("创建文件夹失败 CreateFile %v", err) + return err + } + + usernameNe := conf.Get("ne.user").(string) + // scp user@remote-server:/path/to/remote/directory/ /path/to/local/file.txt + neDir := fmt.Sprintf("%s@%s:%s", usernameNe, neIp, nePath) + cmd := exec.Command("scp", "-r", neDir, localPath) + out, err := cmd.CombinedOutput() + if err != nil { + return err + } + log.Infof("FileSCPNeToLocal %s", string(out)) + return nil +} diff --git a/lib/core/file/txt.go b/lib/core/file/txt.go new file mode 100644 index 0000000..1690e3e --- /dev/null +++ b/lib/core/file/txt.go @@ -0,0 +1,79 @@ +package file + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strings" + + "ems.agt/lib/log" +) + +// 写入Txt文件用,号分割 需要转换数据 +// 例如: +// data := [][]string{} +// data = append(data, []string{"姓名", "年龄", "城市"}) +// data = append(data, []string{"1", "2", "3"}) +// err := file.WriterCSVFile(data, filePath) +func WriterTxtFile(data [][]string, filePath string) error { + // 获取文件所在的目录路径 + dirPath := filepath.Dir(filePath) + + // 确保文件夹路径存在 + err := os.MkdirAll(dirPath, os.ModePerm) + if err != nil { + log.Errorf("创建文件夹失败 CreateFile %v", err) + } + + // 创建或打开文件 + file, err := os.Create(filePath) + if err != nil { + return err + } + defer file.Close() + + // 创建一个 Writer 对象,用于将数据写入文件 + writer := bufio.NewWriter(file) + for _, row := range data { + line := strings.Join(row, ",") + fmt.Fprintln(writer, line) + } + + // 将缓冲区中的数据刷新到文件中 + err = writer.Flush() + if err != nil { + log.Errorf("刷新缓冲区时发生错误:", err) + return err + } + return nil +} + +// 读取Txt文件,用,号分割 转换数组数据 +func ReadTxtFile(filePath string) [][]string { + // 创建 map 存储 CSV 数据 + arr := make([][]string, 0) + + // 打开文本文件 + file, err := os.Open(filePath) + if err != nil { + log.Fatal("无法打开文件:", err) + return arr + } + defer file.Close() + + // 创建一个 Scanner 对象,用于逐行读取文件内容 + scanner := bufio.NewScanner(file) + if scanner.Err() != nil { + log.Fatal("读取文件时出错:", scanner.Err()) + return arr + } + + for scanner.Scan() { + line := scanner.Text() + fields := strings.Split(line, ",") + arr = append(arr, fields) + } + + return arr +} diff --git a/lib/core/mml_client/mml_client.go b/lib/core/mml_client/mml_client.go new file mode 100644 index 0000000..5903123 --- /dev/null +++ b/lib/core/mml_client/mml_client.go @@ -0,0 +1,88 @@ +package mmlclient + +import ( + "bufio" + "fmt" + "io" + "net" + "time" + + "ems.agt/lib/core/conf" +) + +// 定义MMLClient结构体 +type MMLClient struct { + awaitTime time.Duration // 等待时间 + conn net.Conn + reader *bufio.Reader + size int // 包含字符 +} + +// 封装NewMMLClient函数,用于创建MMLClient实例 +// 网元UDM的IP地址 "198.51.100.1" +func NewMMLClient(ip string) (*MMLClient, error) { + // 创建TCP连接 + portMML := conf.Get("mml.port").(int) + hostMML := fmt.Sprintf("%s:%d", ip, portMML) + conn, err := net.Dial("tcp", hostMML) + if err != nil { + return nil, err + } + + // 进行登录 + usernameMML := conf.Get("mml.user").(string) + passwordMML := conf.Get("mml.password").(string) + fmt.Fprintln(conn, usernameMML) + fmt.Fprintln(conn, passwordMML) + + // 发送后等待 + sleepTime := conf.Get("mml.sleep").(int) + awaitTime := time.Duration(sleepTime) * time.Millisecond + time.Sleep(awaitTime) + + // 读取内容 + buf := make([]byte, 1024*8) + n, err := conn.Read(buf) + if err != nil { + return nil, err + } + + // 创建MMLClient实例 + client := &MMLClient{ + conn: conn, + reader: bufio.NewReader(conn), + awaitTime: awaitTime, + size: n, + } + + return client, nil +} + +// 封装Send函数,用于向TCP连接发送数据 +func (c *MMLClient) Send(msg string) error { + _, err := fmt.Fprintln(c.conn, msg) + if err != nil { + return err + } + time.Sleep(c.awaitTime) + return nil +} + +// 封装Receive函数,用于从TCP连接中接收数据 +func (c *MMLClient) Receive() (string, error) { + buf := make([]byte, 1024*8) + n, err := c.reader.Read(buf) + if err != nil { + if err == io.EOF { + return "", fmt.Errorf("server closed the connection") + } + return "", err + } + + return string(buf[0:n]), nil +} + +// 封装Close函数,用于关闭TCP连接 +func (c *MMLClient) Close() error { + return c.conn.Close() +} diff --git a/lib/core/mml_client/send.go b/lib/core/mml_client/send.go new file mode 100644 index 0000000..7e22579 --- /dev/null +++ b/lib/core/mml_client/send.go @@ -0,0 +1,104 @@ +package mmlclient + +import ( + "fmt" + "strings" +) + +// 发送MML原始消息 +// ip 网元IP地址 +// msg 指令 +func MMLSendMsg(ip, msg string) (string, error) { + // 创建MMLClient实例 + client, err := NewMMLClient(ip) + if err != nil { + return "", fmt.Errorf("创建MMLClient实例失败:%v", err) + } + defer client.Close() + + // 发送数据 + err = client.Send(msg) + if err != nil { + return "", fmt.Errorf("发送数据失败:%v", err) + } + + // 接收数据 + data, err := client.Receive() + if err != nil { + return "", fmt.Errorf("接收数据失败:%v", err) + } + + return data, nil +} + +// 发送MML +// ip 网元IP地址 +// msg 指令 +func MMLSendMsgToString(ip, msg string) (string, error) { + // 发送获取数据 + str, err := MMLSendMsg(ip, msg) + if err != nil { + return "", err + } + + // 截断 + index := strings.Index(str, "\n") + if index != -1 { + str = str[:index] + str = strings.ToLower(str) + } + + // 命令成功 + if strings.Contains(str, "ok") || strings.Contains(str, "OK") { + return str, nil + } + + return "", fmt.Errorf(str) +} + +// 发送MML +// ip 网元IP地址 +// msg 指令 +func MMLSendMsgToMap(ip, msg string) (map[string]string, error) { + // 发送获取数据 + str, err := MMLSendMsg(ip, msg) + if err != nil { + return nil, err + } + + // 无数据 + if strings.HasPrefix(str, "No Auth Data") { + return nil, fmt.Errorf("no auth data") + } + + // 初始化一个map用于存储拆分后的键值对 + m := make(map[string]string) + + var items []string + if strings.Contains(str, "\r\n") { + // 按照分隔符"\r\n"进行拆分 + items = strings.Split(str, "\r\n") + } else if strings.Contains(str, "\n") { + // 按照分隔符"\n"进行拆分 + items = strings.Split(str, "\n") + } + + // 遍历拆分后的结果 + for _, item := range items { + var pair []string + + if strings.Contains(item, "=") { + // 按照分隔符"="进行拆分键值对 + pair = strings.Split(item, "=") + } else if strings.Contains(item, ":") { + // 按照分隔符":"进行拆分键值对 + pair = strings.Split(item, ":") + } + + if len(pair) == 2 { + // 将键值对存入map中 + m[pair[0]] = pair[1] + } + } + return m, err +} diff --git a/lib/core/redis/redis.go b/lib/core/redis/redis.go new file mode 100644 index 0000000..d459778 --- /dev/null +++ b/lib/core/redis/redis.go @@ -0,0 +1,358 @@ +package redis + +import ( + "context" + "fmt" + "strings" + "time" + + "ems.agt/lib/core/conf" + "ems.agt/lib/log" + "github.com/redis/go-redis/v9" +) + +// Redis连接实例 +var rdbMap = make(map[string]*redis.Client) + +// 声明定义限流脚本命令 +var rateLimitCommand = redis.NewScript(` +local key = KEYS[1] +local time = tonumber(ARGV[1]) +local count = tonumber(ARGV[2]) +local current = redis.call('get', key); +if current and tonumber(current) >= count then + return tonumber(current); +end +current = redis.call('incr', key) +if tonumber(current) == 1 then + redis.call('expire', key, time) +end +return tonumber(current);`) + +// 连接Redis实例 +func Connect() { + ctx := context.Background() + // 读取数据源配置 + datasource := conf.Get("redis.dataSource").(map[string]any) + for k, v := range datasource { + client := v.(map[string]any) + // 创建连接 + address := fmt.Sprintf("%s:%d", client["host"], client["port"]) + rdb := redis.NewClient(&redis.Options{ + Addr: address, + Password: client["password"].(string), + DB: client["db"].(int), + }) + // 测试数据库连接 + pong, err := rdb.Ping(ctx).Result() + if err != nil { + log.Fatalf("failed error ping redis %s %d is %v", client["host"], client["db"], err) + continue + } + log.Infof("redis %s %d %s connection is successful.", client["host"], client["db"], pong) + rdbMap[k] = rdb + } +} + +// 关闭Redis实例 +func Close() { + for _, rdb := range rdbMap { + if err := rdb.Close(); err != nil { + log.Errorf("fatal error db close: %s", err) + } + } +} + +// 获取默认实例 +func DefaultRDB() *redis.Client { + source := conf.Get("redis.defaultDataSourceName").(string) + return rdbMap[source] +} + +// 获取实例 +func RDB(source string) *redis.Client { + return rdbMap[source] +} + +// Info 获取redis服务信息 +func Info(source string) map[string]map[string]string { + // 数据源 + rdb := DefaultRDB() + if source != "" { + rdb = RDB(source) + } + + ctx := context.Background() + info, err := rdb.Info(ctx).Result() + if err != nil { + return map[string]map[string]string{} + } + infoObj := make(map[string]map[string]string) + lines := strings.Split(info, "\r\n") + label := "" + for _, line := range lines { + if strings.Contains(line, "#") { + label = strings.Fields(line)[len(strings.Fields(line))-1] + label = strings.ToLower(label) + infoObj[label] = make(map[string]string) + continue + } + kvArr := strings.Split(line, ":") + if len(kvArr) >= 2 { + key := strings.TrimSpace(kvArr[0]) + value := strings.TrimSpace(kvArr[len(kvArr)-1]) + infoObj[label][key] = value + } + } + return infoObj +} + +// KeySize 获取redis当前连接可用键Key总数信息 +func KeySize(source string) int64 { + // 数据源 + rdb := DefaultRDB() + if source != "" { + rdb = RDB(source) + } + + ctx := context.Background() + size, err := rdb.DBSize(ctx).Result() + if err != nil { + return 0 + } + return size +} + +// CommandStats 获取redis命令状态信息 +func CommandStats(source string) []map[string]string { + // 数据源 + rdb := DefaultRDB() + if source != "" { + rdb = RDB(source) + } + + ctx := context.Background() + commandstats, err := rdb.Info(ctx, "commandstats").Result() + if err != nil { + return []map[string]string{} + } + statsObjArr := make([]map[string]string, 0) + lines := strings.Split(commandstats, "\r\n") + for _, line := range lines { + if !strings.HasPrefix(line, "cmdstat_") { + continue + } + kvArr := strings.Split(line, ":") + key := kvArr[0] + valueStr := kvArr[len(kvArr)-1] + statsObj := make(map[string]string) + statsObj["name"] = key[8:] + statsObj["value"] = valueStr[6:strings.Index(valueStr, ",usec=")] + statsObjArr = append(statsObjArr, statsObj) + } + return statsObjArr +} + +// 获取键的剩余有效时间(秒) +func GetExpire(source string, key string) (float64, error) { + // 数据源 + rdb := DefaultRDB() + if source != "" { + rdb = RDB(source) + } + + ctx := context.Background() + ttl, err := rdb.TTL(ctx, key).Result() + if err != nil { + return 0, err + } + return ttl.Seconds(), nil +} + +// 获得缓存数据的key列表 +func GetKeys(source string, pattern string) ([]string, error) { + // 数据源 + rdb := DefaultRDB() + if source != "" { + rdb = RDB(source) + } + + // 初始化变量 + var keys []string + var cursor uint64 = 0 + ctx := context.Background() + // 循环遍历获取匹配的键 + for { + // 使用 SCAN 命令获取匹配的键 + batchKeys, nextCursor, err := rdb.Scan(ctx, cursor, pattern, 100).Result() + if err != nil { + log.Errorf("Failed to scan keys: %v", err) + return keys, err + } + cursor = nextCursor + keys = append(keys, batchKeys...) + // 当 cursor 为 0,表示遍历完成 + if cursor == 0 { + break + } + } + return keys, nil +} + +// 批量获得缓存数据 +func GetBatch(source string, keys []string) ([]any, error) { + if len(keys) == 0 { + return []any{}, fmt.Errorf("not keys") + } + + // 数据源 + rdb := DefaultRDB() + if source != "" { + rdb = RDB(source) + } + + // 获取缓存数据 + result, err := rdb.MGet(context.Background(), keys...).Result() + if err != nil { + log.Errorf("Failed to get batch data: %v", err) + return []any{}, err + } + return result, nil +} + +// 获得缓存数据 +func Get(source, key string) (string, error) { + // 数据源 + rdb := DefaultRDB() + if source != "" { + rdb = RDB(source) + } + + ctx := context.Background() + value, err := rdb.Get(ctx, key).Result() + if err == redis.Nil || err != nil { + return "", err + } + return value, nil +} + +// 获得缓存数据Hash +func GetHash(source, key string) (map[string]string, error) { + // 数据源 + rdb := DefaultRDB() + if source != "" { + rdb = RDB(source) + } + + ctx := context.Background() + value, err := rdb.HGetAll(ctx, key).Result() + if err == redis.Nil || err != nil { + return map[string]string{}, err + } + return value, nil +} + +// 判断是否存在 +func Has(source string, keys ...string) (bool, error) { + // 数据源 + rdb := DefaultRDB() + if source != "" { + rdb = RDB(source) + } + + ctx := context.Background() + exists, err := rdb.Exists(ctx, keys...).Result() + if err != nil { + return false, err + } + return exists >= 1, nil +} + +// 设置缓存数据 +func Set(source, key string, value any) (bool, error) { + // 数据源 + rdb := DefaultRDB() + if source != "" { + rdb = RDB(source) + } + + ctx := context.Background() + err := rdb.Set(ctx, key, value, 0).Err() + if err != nil { + log.Errorf("redis lua script err %v", err) + return false, err + } + return true, nil +} + +// 设置缓存数据与过期时间 +func SetByExpire(source, key string, value any, expiration time.Duration) (bool, error) { + // 数据源 + rdb := DefaultRDB() + if source != "" { + rdb = RDB(source) + } + + ctx := context.Background() + err := rdb.Set(ctx, key, value, expiration).Err() + if err != nil { + log.Errorf("redis lua script err %v", err) + return false, err + } + return true, nil +} + +// 删除单个 +func Del(source string, key string) (bool, error) { + // 数据源 + rdb := DefaultRDB() + if source != "" { + rdb = RDB(source) + } + + ctx := context.Background() + err := rdb.Del(ctx, key).Err() + if err != nil { + log.Errorf("redis lua script err %v", err) + return false, err + } + return true, nil +} + +// 删除多个 +func DelKeys(source string, keys []string) (bool, error) { + if len(keys) == 0 { + return false, fmt.Errorf("no keys") + } + + // 数据源 + rdb := DefaultRDB() + if source != "" { + rdb = RDB(source) + } + + ctx := context.Background() + err := rdb.Del(ctx, keys...).Err() + if err != nil { + log.Errorf("redis lua script err %v", err) + return false, err + } + return true, nil +} + +// 限流查询并记录 +func RateLimit(source, limitKey string, time, count int64) (int64, error) { + // 数据源 + rdb := DefaultRDB() + if source != "" { + rdb = RDB(source) + } + + ctx := context.Background() + result, err := rateLimitCommand.Run(ctx, rdb, []string{limitKey}, time, count).Result() + if err != nil { + log.Errorf("redis lua script err %v", err) + return 0, err + } + return result.(int64), err +} diff --git a/lib/core/utils/crypto/crypto.go b/lib/core/utils/crypto/crypto.go new file mode 100644 index 0000000..c242f7f --- /dev/null +++ b/lib/core/utils/crypto/crypto.go @@ -0,0 +1,20 @@ +package crypto + +import ( + "golang.org/x/crypto/bcrypt" +) + +// BcryptHash Bcrypt密码加密 +func BcryptHash(originStr string) string { + hash, err := bcrypt.GenerateFromPassword([]byte(originStr), bcrypt.DefaultCost) + if err != nil { + return "" + } + return string(hash) +} + +// BcryptCompare Bcrypt密码匹配检查 +func BcryptCompare(originStr, hashStr string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hashStr), []byte(originStr)) + return err == nil +} diff --git a/lib/core/utils/ctx/ctx.go b/lib/core/utils/ctx/ctx.go new file mode 100644 index 0000000..f800d7a --- /dev/null +++ b/lib/core/utils/ctx/ctx.go @@ -0,0 +1,133 @@ +package ctx + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path/filepath" + + "ems.agt/lib/core/vo" + "github.com/gorilla/mux" +) + +// Param 地址栏参数{id} +func Param(r *http.Request, key string) string { + vars := mux.Vars(r) + v, ok := vars[key] + if ok { + return v + } + return "" +} + +// GetQuery 查询参数 +func GetQuery(r *http.Request, key string) string { + return r.URL.Query().Get(key) +} + +// QueryMap 查询参数转换Map +func QueryMap(r *http.Request) map[string]any { + queryValues := r.URL.Query() + queryParams := make(map[string]any) + for key, values := range queryValues { + queryParams[key] = values[0] + } + return queryParams +} + +// 读取json请求结构团体 +func ShouldBindJSON(r *http.Request, args any) error { + body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) // 设置较大的长度,例如 1<<20 (1MB) + if err != nil { + return err + } + err = json.Unmarshal(body, args) + return err +} + +// JSON 相应json数据 +func JSON(w http.ResponseWriter, code int, data any) { + w.Header().Set("Content-Type", "application/json;charset=UTF-8") + + response, err := json.Marshal(data) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) + } else { + w.WriteHeader(code) + w.Write(response) + } +} + +// 将文件导出到外部下载 +func FileAttachment(w http.ResponseWriter, r *http.Request, filepath, filename string) { + w.Header().Set("Content-Disposition", `attachment; filename=`+url.QueryEscape(filename)) + w.Header().Set("Content-Type", "application/octet-stream") + http.ServeFile(w, r, filepath) +} + +// 将文件上传保存到指定目录 +// file, handler, err := r.FormFile("file") +// SaveUploadedFile uploads the form file to specific dst. +func SaveUploadedFile(r *http.Request, dst string) error { + // 解析请求中的文件 + _, handler, err := r.FormFile("file") + if err != nil { + return err + } + + src, err := handler.Open() + if err != nil { + return err + } + defer src.Close() + + if err = os.MkdirAll(filepath.Dir(dst), 0750); err != nil { + return err + } + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, src) + return err +} + +/// ==== 登录用户信息, 通过中间件后预置入 + +// 定义自定义类型作为键 +type ContextKey string + +// LoginUser 登录用户信息需要Authorize中间件 +func LoginUser(r *http.Request) (vo.LoginUser, error) { + // 上下文 + v := r.Context().Value(ContextKey("LoginUser")) + if v != nil { + return v.(vo.LoginUser), nil + } + return vo.LoginUser{}, fmt.Errorf("无用户信息") +} + +// LoginUserToUserID 登录用户信息-用户ID +func LoginUserToUserID(r *http.Request) string { + loginUser, err := LoginUser(r) + if err != nil { + return "" + } + return loginUser.UserID +} + +// LoginUserToUserName 登录用户信息-用户名称 +func LoginUserToUserName(r *http.Request) string { + loginUser, err := LoginUser(r) + if err != nil { + return "" + } + return loginUser.UserName +} diff --git a/lib/core/utils/date/date.go b/lib/core/utils/date/date.go new file mode 100644 index 0000000..e0893fb --- /dev/null +++ b/lib/core/utils/date/date.go @@ -0,0 +1,70 @@ +package date + +import ( + "fmt" + "time" + + "ems.agt/lib/log" +) + +const ( + // 年 列如:2022 + YYYY = "2006" + // 年-月 列如:2022-12 + YYYY_MM = "2006-01" + // 年-月-日 列如:2022-12-30 + YYYY_MM_DD = "2006-01-02" + // 年月日时分秒 列如:20221230010159 + YYYYMMDDHHMMSS = "20060102150405" + // 年-月-日 时:分:秒 列如:2022-12-30 01:01:59 + YYYY_MM_DD_HH_MM_SS = "2006-01-02 15:04:05" +) + +// 格式时间字符串 +// +// dateStr 时间字符串 +// +// formatStr 时间格式 默认YYYY-MM-DD HH:mm:ss +func ParseStrToDate(dateStr, formatStr string) time.Time { + t, err := time.Parse(formatStr, dateStr) + if err != nil { + log.Infof("utils ParseStrToDate err %v", err) + return time.Time{} + } + return t +} + +// 格式时间 +// +// date 可转的Date对象 +// +// formatStr 时间格式 默认YYYY-MM-DD HH:mm:ss +func ParseDateToStr(date any, formatStr string) string { + t, ok := date.(time.Time) + if !ok { + switch v := date.(type) { + case int64: + if v == 0 { + return "" + } + t = time.UnixMilli(v) + case string: + parsedTime, err := time.Parse(formatStr, v) + if err != nil { + fmt.Printf("utils ParseDateToStr err %v \n", err) + return "" + } + t = parsedTime + default: + return "" + } + } + return t.Format(formatStr) +} + +// 格式时间成日期路径 +// +// 年/月 列如:2022/12 +func ParseDatePath(date time.Time) string { + return date.Format("2006/01") +} diff --git a/lib/core/utils/firewall/client.go b/lib/core/utils/firewall/client.go new file mode 100644 index 0000000..5e88704 --- /dev/null +++ b/lib/core/utils/firewall/client.go @@ -0,0 +1,34 @@ +package firewall + +import ( + "errors" + "os" + + "ems.agt/lib/core/utils/firewall/client" +) + +type FirewallClient interface { + Name() string // ufw firewalld + Start() error + Stop() error + Reload() error + Status() (string, error) // running not running + Version() (string, error) + + ListPort() ([]client.FireInfo, error) + ListAddress() ([]client.FireInfo, error) + + Port(port client.FireInfo, operation string) error + RichRules(rule client.FireInfo, operation string) error + PortForward(info client.Forward, operation string) error +} + +func NewFirewallClient() (FirewallClient, error) { + if _, err := os.Stat("/usr/sbin/firewalld"); err == nil { + return client.NewFirewalld() + } + if _, err := os.Stat("/usr/sbin/ufw"); err == nil { + return client.NewUfw() + } + return nil, errors.New("no such type") +} diff --git a/lib/core/utils/firewall/client/firewalld.go b/lib/core/utils/firewall/client/firewalld.go new file mode 100644 index 0000000..d026e4e --- /dev/null +++ b/lib/core/utils/firewall/client/firewalld.go @@ -0,0 +1,209 @@ +package client + +import ( + "fmt" + "strings" + "sync" + + "ems.agt/lib/core/cmd" +) + +type Firewall struct{} + +func NewFirewalld() (*Firewall, error) { + return &Firewall{}, nil +} + +func (f *Firewall) Name() string { + return "firewalld" +} + +func (f *Firewall) Status() (string, error) { + stdout, _ := cmd.Exec("firewall-cmd --state") + if stdout == "running\n" { + return "running", nil + } + return "not running", nil +} + +func (f *Firewall) Version() (string, error) { + stdout, err := cmd.Exec("firewall-cmd --version") + if err != nil { + return "", fmt.Errorf("load the firewall version failed, err: %s", stdout) + } + return strings.ReplaceAll(stdout, "\n ", ""), nil +} + +func (f *Firewall) Start() error { + stdout, err := cmd.Exec("systemctl start firewalld") + if err != nil { + return fmt.Errorf("enable the firewall failed, err: %s", stdout) + } + return nil +} + +func (f *Firewall) Stop() error { + stdout, err := cmd.Exec("systemctl stop firewalld") + if err != nil { + return fmt.Errorf("stop the firewall failed, err: %s", stdout) + } + return nil +} + +func (f *Firewall) Reload() error { + stdout, err := cmd.Exec("firewall-cmd --reload") + if err != nil { + return fmt.Errorf("reload firewall failed, err: %s", stdout) + } + return nil +} + +func (f *Firewall) ListPort() ([]FireInfo, error) { + var wg sync.WaitGroup + var datas []FireInfo + wg.Add(2) + go func() { + defer wg.Done() + stdout, err := cmd.Exec("firewall-cmd --zone=public --list-ports") + if err != nil { + return + } + ports := strings.Split(strings.ReplaceAll(stdout, "\n", ""), " ") + for _, port := range ports { + if len(port) == 0 { + continue + } + var itemPort FireInfo + if strings.Contains(port, "/") { + itemPort.Port = strings.Split(port, "/")[0] + itemPort.Protocol = strings.Split(port, "/")[1] + } + itemPort.Strategy = "accept" + datas = append(datas, itemPort) + } + }() + + go func() { + defer wg.Done() + stdout1, err := cmd.Exec("firewall-cmd --zone=public --list-rich-rules") + if err != nil { + return + } + rules := strings.Split(stdout1, "\n") + for _, rule := range rules { + if len(rule) == 0 { + continue + } + itemRule := f.loadInfo(rule) + if len(itemRule.Port) != 0 && itemRule.Family == "ipv4" { + datas = append(datas, itemRule) + } + } + }() + wg.Wait() + return datas, nil +} + +func (f *Firewall) ListAddress() ([]FireInfo, error) { + stdout, err := cmd.Exec("firewall-cmd --zone=public --list-rich-rules") + if err != nil { + return nil, err + } + var datas []FireInfo + rules := strings.Split(stdout, "\n") + for _, rule := range rules { + if len(rule) == 0 { + continue + } + itemRule := f.loadInfo(rule) + if len(itemRule.Port) == 0 && len(itemRule.Address) != 0 { + datas = append(datas, itemRule) + } + } + return datas, nil +} + +func (f *Firewall) Port(port FireInfo, operation string) error { + if cmd.CheckIllegal(operation, port.Protocol, port.Port) { + return fmt.Errorf("errCmdIllegal %v", port) + } + + stdout, err := cmd.Execf("firewall-cmd --zone=public --%s-port=%s/%s --permanent", operation, port.Port, port.Protocol) + if err != nil { + return fmt.Errorf("%s port failed, err: %s", operation, stdout) + } + return nil +} + +func (f *Firewall) RichRules(rule FireInfo, operation string) error { + if cmd.CheckIllegal(operation, rule.Address, rule.Protocol, rule.Port, rule.Strategy) { + return fmt.Errorf("errCmdIllegal %v", rule) + } + ruleStr := "" + if strings.Contains(rule.Address, "-") { + std, err := cmd.Execf("firewall-cmd --permanent --new-ipset=%s --type=hash:ip", rule.Address) + if err != nil { + return fmt.Errorf("add new ipset failed, err: %s", std) + } + std2, err := cmd.Execf("firewall-cmd --permanent --ipset=%s --add-entry=%s", rule.Address, rule.Address) + if err != nil { + return fmt.Errorf("add entry to ipset failed, err: %s", std2) + } + if err := f.Reload(); err != nil { + return err + } + ruleStr = fmt.Sprintf("rule source ipset=%s %s", rule.Address, rule.Strategy) + } else { + ruleStr = "rule family=ipv4 " + if len(rule.Address) != 0 { + ruleStr += fmt.Sprintf("source address=%s ", rule.Address) + } + if len(rule.Port) != 0 { + ruleStr += fmt.Sprintf("port port=%s ", rule.Port) + } + if len(rule.Protocol) != 0 { + ruleStr += fmt.Sprintf("protocol=%s ", rule.Protocol) + } + ruleStr += rule.Strategy + } + stdout, err := cmd.Execf("firewall-cmd --zone=public --%s-rich-rule '%s' --permanent", operation, ruleStr) + if err != nil { + return fmt.Errorf("%s rich rules failed, err: %s", operation, stdout) + } + return nil +} + +func (f *Firewall) PortForward(info Forward, operation string) error { + ruleStr := fmt.Sprintf("firewall-cmd --%s-forward-port=port=%s:proto=%s:toport=%s --permanent", operation, info.Port, info.Protocol, info.Target) + if len(info.Address) != 0 { + ruleStr = fmt.Sprintf("firewall-cmd --%s-forward-port=port=%s:proto=%s:toaddr=%s:toport=%s --permanent", operation, info.Port, info.Protocol, info.Address, info.Target) + } + + stdout, err := cmd.Exec(ruleStr) + if err != nil { + return fmt.Errorf("%s port forward failed, err: %s", operation, stdout) + } + return nil +} + +func (f *Firewall) loadInfo(line string) FireInfo { + var itemRule FireInfo + ruleInfo := strings.Split(strings.ReplaceAll(line, "\"", ""), " ") + for _, item := range ruleInfo { + switch { + case strings.Contains(item, "family="): + itemRule.Family = strings.ReplaceAll(item, "family=", "") + case strings.Contains(item, "ipset="): + itemRule.Address = strings.ReplaceAll(item, "ipset=", "") + case strings.Contains(item, "address="): + itemRule.Address = strings.ReplaceAll(item, "address=", "") + case strings.Contains(item, "port="): + itemRule.Port = strings.ReplaceAll(item, "port=", "") + case strings.Contains(item, "protocol="): + itemRule.Protocol = strings.ReplaceAll(item, "protocol=", "") + case item == "accept" || item == "drop" || item == "reject": + itemRule.Strategy = item + } + } + return itemRule +} diff --git a/lib/core/utils/firewall/client/info.go b/lib/core/utils/firewall/client/info.go new file mode 100644 index 0000000..86cc86e --- /dev/null +++ b/lib/core/utils/firewall/client/info.go @@ -0,0 +1,20 @@ +package client + +type FireInfo struct { + Family string `json:"family"` // ipv4 ipv6 + Address string `json:"address"` // Anywhere + Port string `json:"port"` + Protocol string `json:"protocol"` // tcp udp tcp/udp + Strategy string `json:"strategy"` // accept drop + + APPName string `json:"appName"` + IsUsed bool `json:"isUsed"` + Description string `json:"description"` +} + +type Forward struct { + Protocol string `json:"protocol"` + Address string `json:"address"` + Port string `json:"port"` + Target string `json:"target"` +} diff --git a/lib/core/utils/firewall/client/ufw.go b/lib/core/utils/firewall/client/ufw.go new file mode 100644 index 0000000..4729488 --- /dev/null +++ b/lib/core/utils/firewall/client/ufw.go @@ -0,0 +1,238 @@ +package client + +import ( + "fmt" + "strings" + + "ems.agt/lib/core/cmd" +) + +type Ufw struct { + CmdStr string +} + +func NewUfw() (*Ufw, error) { + var ufw Ufw + if cmd.HasNoPasswordSudo() { + ufw.CmdStr = "sudo ufw" + } else { + ufw.CmdStr = "ufw" + } + return &ufw, nil +} + +func (f *Ufw) Name() string { + return "ufw" +} + +func (f *Ufw) Status() (string, error) { + stdout, _ := cmd.Execf("%s status | grep Status", f.CmdStr) + if stdout == "Status: active\n" { + return "running", nil + } + stdout1, _ := cmd.Execf("%s status | grep 状态", f.CmdStr) + if stdout1 == "状态: 激活\n" { + return "running", nil + } + return "not running", nil +} + +func (f *Ufw) Version() (string, error) { + stdout, err := cmd.Execf("%s version | grep ufw", f.CmdStr) + if err != nil { + return "", fmt.Errorf("load the firewall status failed, err: %s", stdout) + } + info := strings.ReplaceAll(stdout, "\n", "") + return strings.ReplaceAll(info, "ufw ", ""), nil +} + +func (f *Ufw) Start() error { + stdout, err := cmd.Execf("echo y | %s enable", f.CmdStr) + if err != nil { + return fmt.Errorf("enable the firewall failed, err: %s", stdout) + } + return nil +} + +func (f *Ufw) Stop() error { + stdout, err := cmd.Execf("%s disable", f.CmdStr) + if err != nil { + return fmt.Errorf("stop the firewall failed, err: %s", stdout) + } + return nil +} + +func (f *Ufw) Reload() error { + return nil +} + +func (f *Ufw) ListPort() ([]FireInfo, error) { + stdout, err := cmd.Execf("%s status verbose", f.CmdStr) + if err != nil { + return nil, err + } + portInfos := strings.Split(stdout, "\n") + var datas []FireInfo + isStart := false + for _, line := range portInfos { + if strings.HasPrefix(line, "-") { + isStart = true + continue + } + if !isStart { + continue + } + itemFire := f.loadInfo(line, "port") + if len(itemFire.Port) != 0 && itemFire.Port != "Anywhere" && !strings.Contains(itemFire.Port, ".") { + itemFire.Port = strings.ReplaceAll(itemFire.Port, ":", "-") + datas = append(datas, itemFire) + } + } + return datas, nil +} + +func (f *Ufw) ListAddress() ([]FireInfo, error) { + stdout, err := cmd.Execf("%s status verbose", f.CmdStr) + if err != nil { + return nil, err + } + portInfos := strings.Split(stdout, "\n") + var datas []FireInfo + isStart := false + for _, line := range portInfos { + if strings.HasPrefix(line, "-") { + isStart = true + continue + } + if !isStart { + continue + } + if !strings.Contains(line, " IN") { + continue + } + itemFire := f.loadInfo(line, "address") + if strings.Contains(itemFire.Port, ".") { + itemFire.Address += ("-" + itemFire.Port) + itemFire.Port = "" + } + if len(itemFire.Port) == 0 && len(itemFire.Address) != 0 { + datas = append(datas, itemFire) + } + } + return datas, nil +} + +func (f *Ufw) Port(port FireInfo, operation string) error { + switch port.Strategy { + case "accept": + port.Strategy = "allow" + case "drop": + port.Strategy = "deny" + default: + return fmt.Errorf("unsupport strategy %s", port.Strategy) + } + if cmd.CheckIllegal(port.Protocol, port.Port) { + return fmt.Errorf("errCmdIllegal %v", port) + } + + command := fmt.Sprintf("%s %s %s", f.CmdStr, port.Strategy, port.Port) + if operation == "remove" { + command = fmt.Sprintf("%s delete %s %s", f.CmdStr, port.Strategy, port.Port) + } + if len(port.Protocol) != 0 { + command += fmt.Sprintf("/%s", port.Protocol) + } + stdout, err := cmd.Exec(command) + if err != nil { + return fmt.Errorf("%s port failed, err: %s", operation, stdout) + } + return nil +} + +func (f *Ufw) RichRules(rule FireInfo, operation string) error { + switch rule.Strategy { + case "accept": + rule.Strategy = "allow" + case "drop": + rule.Strategy = "deny" + default: + return fmt.Errorf("unsupport strategy %s", rule.Strategy) + } + + if cmd.CheckIllegal(operation, rule.Protocol, rule.Address, rule.Port) { + return fmt.Errorf("errCmdIllegal %v", rule) + } + + ruleStr := fmt.Sprintf("%s %s ", f.CmdStr, rule.Strategy) + if operation == "remove" { + ruleStr = fmt.Sprintf("%s delete %s ", f.CmdStr, rule.Strategy) + } + if len(rule.Protocol) != 0 { + ruleStr += fmt.Sprintf("proto %s ", rule.Protocol) + } + if strings.Contains(rule.Address, "-") { + ruleStr += fmt.Sprintf("from %s to %s ", strings.Split(rule.Address, "-")[0], strings.Split(rule.Address, "-")[1]) + } else { + ruleStr += fmt.Sprintf("from %s ", rule.Address) + } + if len(rule.Port) != 0 { + ruleStr += fmt.Sprintf("to any port %s ", rule.Port) + } + + stdout, err := cmd.Exec(ruleStr) + if err != nil { + return fmt.Errorf("%s rich rules failed, err: %s", operation, stdout) + } + return nil +} + +func (f *Ufw) PortForward(info Forward, operation string) error { + ruleStr := fmt.Sprintf("firewall-cmd --%s-forward-port=port=%s:proto=%s:toport=%s --permanent", operation, info.Port, info.Protocol, info.Target) + if len(info.Address) != 0 { + ruleStr = fmt.Sprintf("firewall-cmd --%s-forward-port=port=%s:proto=%s:toaddr=%s:toport=%s --permanent", operation, info.Port, info.Protocol, info.Address, info.Target) + } + + stdout, err := cmd.Exec(ruleStr) + if err != nil { + return fmt.Errorf("%s port forward failed, err: %s", operation, stdout) + } + if err := f.Reload(); err != nil { + return err + } + return nil +} + +func (f *Ufw) loadInfo(line string, fireType string) FireInfo { + fields := strings.Fields(line) + var itemInfo FireInfo + if len(fields) < 4 { + return itemInfo + } + if fields[1] == "(v6)" { + return itemInfo + } + if fields[0] == "Anywhere" && fireType != "port" { + itemInfo.Strategy = "drop" + if fields[1] == "ALLOW" { + itemInfo.Strategy = "accept" + } + itemInfo.Address = fields[3] + return itemInfo + } + if strings.Contains(fields[0], "/") { + itemInfo.Port = strings.Split(fields[0], "/")[0] + itemInfo.Protocol = strings.Split(fields[0], "/")[1] + } else { + itemInfo.Port = fields[0] + itemInfo.Protocol = "tcp/udp" + } + itemInfo.Family = "ipv4" + if fields[1] == "ALLOW" { + itemInfo.Strategy = "accept" + } else { + itemInfo.Strategy = "drop" + } + itemInfo.Address = fields[3] + + return itemInfo +} diff --git a/lib/core/utils/parse/parse.go b/lib/core/utils/parse/parse.go new file mode 100644 index 0000000..ed2fd5d --- /dev/null +++ b/lib/core/utils/parse/parse.go @@ -0,0 +1,139 @@ +package parse + +import ( + "fmt" + "reflect" + "regexp" + "strconv" + "strings" + "time" + + "github.com/robfig/cron/v3" +) + +// Number 解析数值型 +func Number(str any) int64 { + switch str := str.(type) { + case string: + if str == "" { + return 0 + } + num, err := strconv.ParseInt(str, 10, 64) + if err != nil { + return 0 + } + return num + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + return reflect.ValueOf(str).Int() + case float32, float64: + return int64(reflect.ValueOf(str).Float()) + default: + return 0 + } +} + +// Boolean 解析布尔型 +func Boolean(str any) bool { + switch str := str.(type) { + case string: + if str == "" || str == "false" || str == "0" { + return false + } + // 尝试将字符串解析为数字 + if num, err := strconv.ParseFloat(str, 64); err == nil { + return num != 0 + } + return true + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + num := reflect.ValueOf(str).Int() + return num != 0 + case float32, float64: + num := reflect.ValueOf(str).Float() + return num != 0 + default: + return false + } +} + +// FirstUpper 首字母转大写 +// +// 字符串 abc_123!@# 结果 Abc_123 +func FirstUpper(str string) string { + if len(str) == 0 { + return str + } + reg := regexp.MustCompile(`[^_\w]+`) + str = reg.ReplaceAllString(str, "") + return strings.ToUpper(str[:1]) + str[1:] +} + +// Bit 比特位为单位 +func Bit(bit float64) string { + var GB, MB, KB string + + if bit > float64(1<<30) { + GB = fmt.Sprintf("%0.2f", bit/(1<<30)) + } + + if bit > float64(1<<20) && bit < (1<<30) { + MB = fmt.Sprintf("%.2f", bit/(1<<20)) + } + + if bit > float64(1<<10) && bit < (1<<20) { + KB = fmt.Sprintf("%.2f", bit/(1<<10)) + } + + if GB != "" { + return GB + "GB" + } else if MB != "" { + return MB + "MB" + } else if KB != "" { + return KB + "KB" + } else { + return fmt.Sprintf("%vB", bit) + } +} + +// CronExpression 解析 Cron 表达式,返回下一次执行的时间戳(毫秒) +// +// 【*/5 * * * * ?】 6个参数 +func CronExpression(expression string) int64 { + specParser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor) + schedule, err := specParser.Parse(expression) + if err != nil { + fmt.Println(err) + return 0 + } + return schedule.Next(time.Now()).UnixMilli() +} + +// SafeContent 内容值进行安全掩码 +func SafeContent(value string) string { + if len(value) < 3 { + return strings.Repeat("*", len(value)) + } else if len(value) < 6 { + return string(value[0]) + strings.Repeat("*", len(value)-1) + } else if len(value) < 10 { + return string(value[0]) + strings.Repeat("*", len(value)-2) + string(value[len(value)-1]) + } else if len(value) < 15 { + return value[:2] + strings.Repeat("*", len(value)-4) + value[len(value)-2:] + } else { + return value[:3] + strings.Repeat("*", len(value)-6) + value[len(value)-3:] + } +} + +// RemoveDuplicates 数组内字符串去重 +func RemoveDuplicates(ids []string) []string { + uniqueIDs := make(map[string]bool) + uniqueIDSlice := make([]string, 0) + + for _, id := range ids { + _, ok := uniqueIDs[id] + if !ok && id != "" { + uniqueIDs[id] = true + uniqueIDSlice = append(uniqueIDSlice, id) + } + } + + return uniqueIDSlice +} diff --git a/lib/core/utils/regular/regular.go b/lib/core/utils/regular/regular.go new file mode 100644 index 0000000..e4b9cc6 --- /dev/null +++ b/lib/core/utils/regular/regular.go @@ -0,0 +1,54 @@ +package regular + +import ( + "regexp" +) + +// Replace 正则替换 +func Replace(originStr, pattern, repStr string) string { + regex := regexp.MustCompile(pattern) + return regex.ReplaceAllString(originStr, repStr) +} + +// 判断是否为有效用户名格式 +// +// 用户名不能以数字开头,可包含大写小写字母,数字,且不少于5位 +func ValidUsername(username string) bool { + if username == "" { + return false + } + pattern := `^[a-zA-Z][a-z0-9A-Z]{5,}` + match, err := regexp.MatchString(pattern, username) + if err != nil { + return false + } + return match +} + +// 判断是否为有效手机号格式,1开头的11位手机号 +func ValidMobile(mobile string) bool { + if mobile == "" { + return false + } + pattern := `^1[3|4|5|6|7|8|9][0-9]\d{8}$` + match, err := regexp.MatchString(pattern, mobile) + if err != nil { + return false + } + return match +} + +// 判断是否为http(s)://开头 +// +// link 网络链接 +func ValidHttp(link string) bool { + if link == "" { + return false + } + pattern := `^http(s)?:\/\/+` + match, err := regexp.MatchString(pattern, link) + if err != nil { + return false + } + return match +} diff --git a/lib/core/utils/scan/scan.go b/lib/core/utils/scan/scan.go new file mode 100644 index 0000000..6313763 --- /dev/null +++ b/lib/core/utils/scan/scan.go @@ -0,0 +1,31 @@ +package scan + +import ( + "net" + "strconv" +) + +func ScanPort(port int) bool { + ln, err := net.Listen("tcp", ":"+strconv.Itoa(port)) + if err != nil { + return true + } + defer ln.Close() + return false +} + +func ScanUDPPort(port int) bool { + ln, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: port}) + if err != nil { + return true + } + defer ln.Close() + return false +} + +func ScanPortWithProto(port int, proto string) bool { + if proto == "udp" { + return ScanUDPPort(port) + } + return ScanPort(port) +} diff --git a/lib/core/vo/login_user.go b/lib/core/vo/login_user.go new file mode 100644 index 0000000..4a36331 --- /dev/null +++ b/lib/core/vo/login_user.go @@ -0,0 +1,28 @@ +package vo + +import ( + "ems.agt/lib/dborm" +) + +// LoginUser 登录用户身份权限信息对象 +type LoginUser struct { + // UserID 用户ID + UserID string `json:"userId"` + + // UserName 用户名 + UserName string `json:"userName"` + + // LoginTime 登录时间时间戳 + LoginTime int64 `json:"loginTime"` + + // ExpireTime 过期时间时间戳 + ExpireTime int64 `json:"expireTime"` + + // Permissions 权限列表 + Permissions []string `json:"permissions"` + + // User 用户信息 + User dborm.User `json:"user"` + + Session dborm.Session `json:"-"` +} diff --git a/lib/core/vo/result/result.go b/lib/core/vo/result/result.go new file mode 100644 index 0000000..51faf08 --- /dev/null +++ b/lib/core/vo/result/result.go @@ -0,0 +1,72 @@ +package result + +const CODE_ERROR = 0 +const MSG_ERROR = "error" +const CODE_SUCCESS = 1 +const MSG_SUCCESS = "success" + +// CodeMsg 响应结果 +func CodeMsg(code int, msg string) map[string]any { + args := make(map[string]any) + args["code"] = code + args["msg"] = msg + return args +} + +// 响应成功结果 map[string]any{} +func Ok(v map[string]any) map[string]any { + args := make(map[string]any) + args["code"] = CODE_SUCCESS + args["msg"] = MSG_SUCCESS + // v合并到args + for key, value := range v { + args[key] = value + } + return args +} + +// 响应成功结果信息 +func OkMsg(msg string) map[string]any { + args := make(map[string]any) + args["code"] = CODE_SUCCESS + args["msg"] = msg + return args +} + +// 响应成功结果数据 +func OkData(data any) map[string]any { + args := make(map[string]any) + args["code"] = CODE_SUCCESS + args["msg"] = MSG_SUCCESS + args["data"] = data + return args +} + +// 响应失败结果 map[string]any{} +func Err(v map[string]any) map[string]any { + args := make(map[string]any) + args["code"] = CODE_ERROR + args["msg"] = MSG_ERROR + // v合并到args + for key, value := range v { + args[key] = value + } + return args +} + +// 响应失败结果信息 +func ErrMsg(msg string) map[string]any { + args := make(map[string]any) + args["code"] = CODE_ERROR + args["msg"] = msg + return args +} + +// 响应失败结果数据 +func ErrData(data any) map[string]any { + args := make(map[string]any) + args["code"] = CODE_ERROR + args["msg"] = MSG_ERROR + args["data"] = data + return args +} diff --git a/lib/core/vo/router.go b/lib/core/vo/router.go new file mode 100644 index 0000000..a3cc408 --- /dev/null +++ b/lib/core/vo/router.go @@ -0,0 +1,17 @@ +package vo + +// Router 路由信息对象 +type Router struct { + // 路由名字 英文首字母大写 + Name string `json:"name"` + // 路由地址 + Path string `json:"path"` + // 其他元素 + Meta RouterMeta `json:"meta"` + // 组件地址 + Component string `json:"component"` + // 重定向地址 + Redirect string `json:"redirect"` + // 子路由 + Children []Router `json:"children,omitempty"` +} diff --git a/lib/core/vo/router_meta.go b/lib/core/vo/router_meta.go new file mode 100644 index 0000000..b3447e0 --- /dev/null +++ b/lib/core/vo/router_meta.go @@ -0,0 +1,17 @@ +package vo + +// RouterMeta 路由元信息对象 +type RouterMeta struct { + // 设置该菜单在侧边栏和面包屑中展示的名字 + Title string `json:"title"` + // 设置该菜单的图标 + Icon string `json:"icon"` + // 设置为true,则不会被