| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604 |
- // Copyright 2019 Yunion
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- package handler
- import (
- "bytes"
- "context"
- "encoding/csv"
- "fmt"
- "net/http"
- "strconv"
- "strings"
- "time"
- "github.com/360EntSecGroup-Skylar/excelize"
- "golang.org/x/sync/errgroup"
- "yunion.io/x/jsonutils"
- "yunion.io/x/log"
- "yunion.io/x/pkg/appctx"
- "yunion.io/x/pkg/util/httputils"
- "yunion.io/x/pkg/utils"
- "yunion.io/x/onecloud/pkg/apigateway/options"
- "yunion.io/x/onecloud/pkg/appsrv"
- "yunion.io/x/onecloud/pkg/httperrors"
- "yunion.io/x/onecloud/pkg/mcclient"
- "yunion.io/x/onecloud/pkg/mcclient/auth"
- "yunion.io/x/onecloud/pkg/mcclient/modulebase"
- "yunion.io/x/onecloud/pkg/mcclient/modules/compute"
- "yunion.io/x/onecloud/pkg/mcclient/modules/identity"
- )
- const contentTypeSpreadsheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
- const (
- HOST_MAC = "*MAC地址"
- HOST_NAME = "*名称"
- HOST_IPMI_ADDR = "*IPMI地址"
- HOST_IPMI_USERNAME = "*IPMI用户名"
- HOST_IPMI_PASSWORD = "*IPMI密码"
- HOST_MNG_IP_ADDR = "*管理口IP地址"
- HOST_IPMI_ADDR_OPTIONAL = "IPMI地址"
- HOST_IPMI_USERNAME_OPTIONAL = "IPMI用户名"
- HOST_IPMI_PASSWORD_OPTIONAL = "IPMI密码"
- HOST_MNG_IP_ADDR_OPTIONAL = "管理口IP地址"
- HOST_MNG_MAC_ADDR_OPTIONAL = "管理口MAC地址"
- )
- const (
- BATCH_USER_REGISTER_QUANTITY_LIMITATION = 1000
- BATCH_HOST_REGISTER_QUANTITY_LIMITATION = 1000
- )
- var (
- BatchHostRegisterTemplate = []string{HOST_MAC, HOST_NAME, HOST_IPMI_ADDR_OPTIONAL, HOST_IPMI_USERNAME_OPTIONAL, HOST_IPMI_PASSWORD_OPTIONAL}
- BatchHostISORegisterTemplate = []string{HOST_NAME, HOST_IPMI_ADDR, HOST_IPMI_USERNAME, HOST_IPMI_PASSWORD, HOST_MNG_IP_ADDR}
- BatchHostPXERegisterTemplate = []string{HOST_NAME, HOST_IPMI_ADDR, HOST_IPMI_USERNAME, HOST_IPMI_PASSWORD, HOST_MNG_MAC_ADDR_OPTIONAL, HOST_MNG_IP_ADDR_OPTIONAL}
- )
- func FetchSession(ctx context.Context, r *http.Request) *mcclient.ClientSession {
- token := AppContextToken(ctx)
- session := auth.GetSession(ctx, token, FetchRegion(r))
- return session
- }
- type MiscHandler struct {
- prefix string
- }
- func NewMiscHandler(prefix string) *MiscHandler {
- return &MiscHandler{prefix}
- }
- func (h *MiscHandler) GetPrefix() string {
- return h.prefix
- }
- func (h *MiscHandler) Bind(app *appsrv.Application) {
- prefix := h.prefix
- uploader := UploadHandlerInfo(POST, prefix+"uploads", FetchAuthToken(h.PostUploads))
- app.AddHandler3(uploader)
- app.AddHandler(GET, prefix+"downloads/<template_id>", FetchAuthToken(h.getDownloadsHandler))
- // download vm image by url (no auth token required). token 有效期24小时
- imageDownloadByUrl := uploadHandlerInfo(GET, prefix+"imageutils/image/<image_name>", imageDownloadByUrlHandler)
- app.AddHandler3(imageDownloadByUrl)
- // fetch vm image download url or download large file directly with query parameter <direct=true>
- imageDownloader := uploadHandlerInfo("GET", prefix+"imageutils/download/<image_id>", FetchAuthToken(imageDownloadHandler))
- app.AddHandler3(imageDownloader)
- imageUploader := uploadHandlerInfo("POST", prefix+"imageutils/upload", FetchAuthToken(imageUploadHandler))
- app.AddHandler3(imageUploader)
- s3upload := uploadHandlerInfo(POST, prefix+"s3uploads", FetchAuthToken(h.postS3UploadHandler))
- app.AddHandler3(s3upload)
- // mcp agent chat stream
- chatStream := chatHandlerInfo("POST", prefix+"mcp_agents/<id>/chat-stream", FetchAuthToken(mcpAgentChatStreamHandler))
- app.AddHandler3(chatStream)
- // mcp agent default chat stream (uses agent with default_agent=true)
- defaultChatStream := chatHandlerInfo("POST", prefix+"mcp_agents/default/chat-stream", FetchAuthToken(mcpAgentDefaultChatStreamHandler))
- app.AddHandler3(defaultChatStream)
- // syslog webservice handlers
- app.AddHandler(POST, prefix+"syslog/token", handleSyslogWebServiceToken)
- app.AddHandler(POST, prefix+"syslog/message", handleSyslogWebServiceMessage)
- // service settings
- app.AddHandler(GET, prefix+"service_settings", h.getServiceSettings)
- // mcp servers config
- app.AddHandler(GET, prefix+"mcp-servers-config", mcpServersConfigHandler)
- // mcp agent default MCP server tools (options.MCPServerURL only, no mcp_agent entry)
- app.AddHandler(GET, prefix+"default-mcp-tools", FetchAuthToken(mcpAgentDefaultToolsHandler))
- }
- func UploadHandlerInfo(method, prefix string, handler func(context.Context, http.ResponseWriter, *http.Request)) *appsrv.SHandlerInfo {
- hi := appsrv.SHandlerInfo{}
- hi.SetMethod(method)
- hi.SetPath(prefix)
- hi.SetHandler(handler)
- hi.SetProcessTimeout(6 * time.Hour)
- hi.SetWorkerManager(GetUploaderWorker())
- return &hi
- }
- func (mh *MiscHandler) PostUploads(ctx context.Context, w http.ResponseWriter, req *http.Request) {
- // 10 MB
- var maxMemory int64 = 10 << 20
- e := req.ParseMultipartForm(maxMemory)
- if e != nil {
- httperrors.InvalidInputError(ctx, w, "invalid form")
- return
- }
- params := req.MultipartForm.Value
- actions, ok := params["action"]
- if !ok || len(actions) == 0 || len(actions[0]) == 0 {
- err := httperrors.NewInputParameterError("Missing parameter %s", "action")
- httperrors.JsonClientError(ctx, w, err)
- return
- }
- switch actions[0] {
- // 主机批量注册
- case "BatchHostRegister":
- mh.DoBatchHostRegister(ctx, w, req)
- return
- // 用户批量注册
- case "BatchUserRegister":
- mh.DoBatchUserRegister(ctx, w, req)
- return
- default:
- err := httperrors.NewInputParameterError("Unsupported action %s", actions[0])
- httperrors.JsonClientError(ctx, w, err)
- return
- }
- }
- func (mh *MiscHandler) DoBatchHostRegister(ctx context.Context, w http.ResponseWriter, req *http.Request) {
- files := req.MultipartForm.File
- hostfiles, ok := files["hosts"]
- if !ok || len(hostfiles) == 0 || hostfiles[0] == nil {
- e := httperrors.NewInputParameterError("Missing parameter %s", "hosts")
- httperrors.JsonClientError(ctx, w, e)
- return
- }
- fileHeader := hostfiles[0].Header
- contentType := fileHeader.Get("Content-Type")
- if contentType != contentTypeSpreadsheet {
- e := httperrors.NewInputParameterError("Wrong content type %s, want %s", contentType, contentTypeSpreadsheet)
- httperrors.JsonClientError(ctx, w, e)
- return
- }
- file, err := hostfiles[0].Open()
- defer file.Close()
- if err != nil {
- log.Errorf("%s", err.Error())
- e := httperrors.NewInternalServerError("can't open file")
- httperrors.JsonClientError(ctx, w, e)
- return
- }
- xlsx, err := excelize.OpenReader(file)
- if err != nil {
- log.Errorf("%s", err.Error())
- e := httperrors.NewInternalServerError("can't parse file")
- httperrors.JsonClientError(ctx, w, e)
- return
- }
- rows := xlsx.GetRows("hosts")
- if len(rows) == 0 {
- e := httperrors.NewGeneralError(fmt.Errorf("empty file content"))
- httperrors.JsonClientError(ctx, w, e)
- return
- }
- // check header line
- titlesOk := false
- for _, t := range [][]string{BatchHostRegisterTemplate, BatchHostISORegisterTemplate, BatchHostPXERegisterTemplate} {
- if len(t) == len(rows[0]) {
- for _, title := range rows[0] {
- if !utils.IsInStringArray(title, t) {
- break
- }
- }
- titlesOk = true
- }
- }
- if !titlesOk {
- httperrors.InputParameterError(ctx, w, "template file is invalid. please check.")
- return
- }
- paramKeys := []string{}
- i1 := -1
- i2 := -1
- for i, title := range rows[0] {
- switch title {
- case HOST_MAC, HOST_MNG_MAC_ADDR_OPTIONAL:
- paramKeys = append(paramKeys, "access_mac")
- case HOST_NAME:
- paramKeys = append(paramKeys, "name")
- case HOST_IPMI_ADDR, HOST_IPMI_ADDR_OPTIONAL:
- i1 = i
- paramKeys = append(paramKeys, "ipmi_ip_addr")
- case HOST_IPMI_USERNAME, HOST_IPMI_USERNAME_OPTIONAL:
- paramKeys = append(paramKeys, "ipmi_username")
- case HOST_IPMI_PASSWORD, HOST_IPMI_PASSWORD_OPTIONAL:
- paramKeys = append(paramKeys, "ipmi_password")
- case HOST_MNG_IP_ADDR, HOST_MNG_IP_ADDR_OPTIONAL:
- i2 = i
- paramKeys = append(paramKeys, "access_ip")
- default:
- e := httperrors.NewInternalServerError("empty file content")
- httperrors.JsonClientError(ctx, w, e)
- return
- }
- }
- // skipped header row
- if len(rows) > BATCH_HOST_REGISTER_QUANTITY_LIMITATION {
- e := httperrors.NewInputParameterError("beyond limitation. excel file rows must less than %d", BATCH_HOST_REGISTER_QUANTITY_LIMITATION)
- httperrors.JsonClientError(ctx, w, e)
- return
- }
- ips := []string{}
- hosts := bytes.Buffer{}
- for idx, row := range rows[1:] {
- rowStr := strings.Join(row, "")
- if len(rowStr) == 0 {
- log.Warningf("empty row: %d, skipping it", idx+1)
- continue
- }
- var e *httputils.JSONClientError
- if i1 >= 0 && len(row[i1]) > 0 {
- i1Ip := fmt.Sprintf("%d-%s", i1, row[i1])
- if utils.IsInStringArray(i1Ip, ips) {
- e = httperrors.NewDuplicateIdError("ip", row[i1])
- } else {
- ips = append(ips, i1Ip)
- }
- }
- if i2 >= 0 && len(row[i2]) > 0 {
- i2Ip := fmt.Sprintf("%d-%s", i2, row[i2])
- if utils.IsInStringArray(i2Ip, ips) {
- e = httperrors.NewDuplicateIdError("ip", row[i2])
- } else {
- ips = append(ips, i2Ip)
- }
- }
- if e != nil {
- httperrors.JsonClientError(ctx, w, e)
- return
- }
- hosts.WriteString(strings.Join(row, ",") + "\n")
- }
- params := jsonutils.NewDict()
- s := FetchSession(ctx, req)
- params.Set("hosts", jsonutils.NewString(hosts.String()))
- // extra params
- for k, values := range req.MultipartForm.Value {
- if len(values) > 0 && k != "action" {
- params.Set(k, jsonutils.NewString(values[0]))
- }
- }
- submitResult, err := compute.Hosts.BatchRegister(s, paramKeys, params)
- if err != nil {
- e := httperrors.NewGeneralError(err)
- httperrors.JsonClientError(ctx, w, e)
- return
- }
- w.WriteHeader(207)
- appsrv.SendJSON(w, modulebase.SubmitResults2JSON(submitResult))
- }
- func (mh *MiscHandler) DoBatchUserRegister(ctx context.Context, w http.ResponseWriter, req *http.Request) {
- adminS := auth.GetAdminSession(ctx, FetchRegion(req))
- s := FetchSession(ctx, req)
- files := req.MultipartForm.File
- userfiles, ok := files["users"]
- if !ok || len(userfiles) == 0 || userfiles[0] == nil {
- e := httperrors.NewInputParameterError("Missing parameter %s", "users")
- httperrors.JsonClientError(ctx, w, e)
- return
- }
- fileHeader := userfiles[0].Header
- contentType := fileHeader.Get("Content-Type")
- if contentType != contentTypeSpreadsheet {
- e := httperrors.NewInputParameterError("Wrong content type %s, want %s", contentType, contentTypeSpreadsheet)
- httperrors.JsonClientError(ctx, w, e)
- return
- }
- file, err := userfiles[0].Open()
- defer file.Close()
- if err != nil {
- log.Errorf("%s", err.Error())
- e := httperrors.NewInternalServerError("can't open file")
- httperrors.JsonClientError(ctx, w, e)
- return
- }
- xlsx, err := excelize.OpenReader(file)
- if err != nil {
- log.Errorf("%s", err.Error())
- e := httperrors.NewInternalServerError("can't parse file")
- httperrors.JsonClientError(ctx, w, e)
- return
- }
- // skipped header row
- rows := xlsx.GetRows("users")
- if len(rows) <= 1 {
- e := httperrors.NewInputParameterError("empty file content")
- httperrors.JsonClientError(ctx, w, e)
- return
- } else if len(rows) > BATCH_USER_REGISTER_QUANTITY_LIMITATION {
- e := httperrors.NewInputParameterError("beyond limitation.excel file rows must less than %d", BATCH_USER_REGISTER_QUANTITY_LIMITATION)
- httperrors.JsonClientError(ctx, w, e)
- return
- }
- users := []jsonutils.JSONObject{}
- names := map[string]bool{}
- domains := map[string]string{}
- for i, row := range rows[1:] {
- rowIdx := i + 2
- name := row[0]
- password := row[1]
- displayname := row[2]
- domain := row[3]
- allowWebConsole := strings.ToLower(row[4])
- // 忽略空白行
- rowStr := strings.Join(row, "")
- if len(strings.TrimSpace(rowStr)) == 0 {
- continue
- }
- if len(name) == 0 {
- e := httperrors.NewClientError("row %d name is empty", rowIdx)
- httperrors.JsonClientError(ctx, w, e)
- return
- }
- if len(displayname) == 0 {
- displayname = name
- }
- if len(password) == 0 {
- e := httperrors.NewClientError("row %d password is empty", rowIdx)
- httperrors.JsonClientError(ctx, w, e)
- return
- }
- domainId, ok := domains[domain]
- if !ok {
- if len(domain) == 0 {
- e := httperrors.NewClientError("row %d domain is empty", rowIdx)
- httperrors.JsonClientError(ctx, w, e)
- return
- }
- id, err := identity.Domains.GetId(adminS, domain, nil)
- if err != nil {
- httperrors.JsonClientError(ctx, w, httperrors.NewGeneralError(err))
- return
- }
- domainId = id
- domains[domain] = id
- }
- if _, ok := names[name+"/"+domainId]; ok {
- e := httperrors.NewClientError("row %d duplicate name %s", rowIdx, name)
- httperrors.JsonClientError(ctx, w, e)
- return
- } else {
- names[name+"/"+domainId] = true
- params := jsonutils.NewDict()
- params.Set("domain_id", jsonutils.NewString(domainId))
- _, err := identity.UsersV3.Get(s, name, params)
- if err == nil {
- continue
- }
- }
- user := jsonutils.NewDict()
- user.Add(jsonutils.NewString(name), "name")
- user.Add(jsonutils.NewString(displayname), "displayname")
- user.Add(jsonutils.NewString(domainId), "domain_id")
- if len(password) > 0 {
- user.Add(jsonutils.NewString(password), "password")
- user.Add(jsonutils.JSONTrue, "skip_password_complexity_check")
- }
- if allowWebConsole == "true" || allowWebConsole == "1" {
- user.Add(jsonutils.JSONTrue, "allow_web_console")
- } else {
- user.Add(jsonutils.JSONFalse, "allow_web_console")
- }
- users = append(users, user)
- }
- // batch create
- var userG errgroup.Group
- for i := range users {
- user := users[i]
- userG.Go(func() error {
- _, err := identity.UsersV3.Create(s, user)
- return err
- })
- }
- if err := userG.Wait(); err != nil {
- e := httperrors.NewGeneralError(err)
- httperrors.GeneralServerError(ctx, w, e)
- return
- }
- appsrv.SendJSON(w, jsonutils.NewDict())
- }
- func (mh *MiscHandler) getDownloadsHandler(ctx context.Context, w http.ResponseWriter, req *http.Request) {
- params := appctx.AppContextParams(ctx)
- template, ok := params["<template_id>"]
- if !ok || len(template) == 0 {
- httperrors.MissingParameterError(ctx, w, "template_id")
- return
- }
- var err error
- var content bytes.Buffer
- switch template {
- case "BatchHostRegister":
- records := [][]string{BatchHostRegisterTemplate}
- content, err = writeXlsx("hosts", records)
- case "BatchHostISORegister":
- records := [][]string{BatchHostISORegisterTemplate}
- content, err = writeXlsx("hosts", records)
- case "BatchHostPXERegister":
- records := [][]string{BatchHostPXERegisterTemplate}
- content, err = writeXlsx("hosts", records)
- case "BatchUserRegister":
- records := [][]string{{"*用户名(user)", "*用户密码(password)", "*显示名(displayname)", "*部门/域(domain)", "*是否登录控制台(allow_web_console:true、false)"}}
- content, err = writeXlsx("users", records)
- case "BatchProjectRegister":
- var titles []string
- if options.Options.NonDefaultDomainProjects {
- titles = []string{"项目名称", "域", "配额"}
- } else {
- titles = []string{"项目名称", "域"}
- }
- records := [][]string{titles}
- content, err = writeXlsx("projects", records)
- default:
- httperrors.InputParameterError(ctx, w, "template not found %s", template)
- return
- }
- if err != nil {
- httperrors.InternalServerError(ctx, w, "internal server error")
- return
- }
- w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
- w.Header().Set("Content-Disposition", "Attachment; filename=template.xlsx")
- w.Write(content.Bytes())
- return
- }
- func (mh *MiscHandler) postS3UploadHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
- reader, e := r.MultipartReader()
- if e != nil {
- log.Debugf("postS3UploadHandler.MultipartReader %s", e)
- httperrors.InvalidInputError(ctx, w, "invalid form")
- return
- }
- p, f, e := readImageForm(reader)
- if e != nil {
- log.Debugf("postS3UploadHandler.readImageForm %s", e)
- httperrors.InvalidInputError(ctx, w, "invalid form")
- return
- }
- bucket_id, ok := p["bucket_id"]
- if !ok {
- httperrors.MissingParameterError(ctx, w, "bucket_id")
- return
- }
- key, ok := p["key"]
- if !ok {
- httperrors.MissingParameterError(ctx, w, "key")
- return
- }
- _content_length, ok := p["content_length"]
- if !ok {
- httperrors.MissingParameterError(ctx, w, "content_length")
- return
- }
- content_length, e := strconv.ParseInt(_content_length, 10, 64)
- if e != nil {
- httperrors.InvalidInputError(ctx, w, "invalid content_length %s", _content_length)
- return
- }
- storage_class, _ := p["storage_class"]
- acl, _ := p["acl"]
- token := AppContextToken(ctx)
- s := auth.GetSession(ctx, token, FetchRegion(r))
- meta := http.Header{}
- meta.Set("Content-Type", "application/octet-stream")
- e = compute.Buckets.Upload(s, bucket_id, key, f, content_length, storage_class, acl, meta)
- if e != nil {
- httperrors.GeneralServerError(ctx, w, e)
- return
- }
- appsrv.SendJSON(w, jsonutils.NewDict())
- }
- func writeCsv(records [][]string) (bytes.Buffer, error) {
- var content bytes.Buffer
- content.WriteString("\xEF\xBB\xBF") // 写入UTF-8 BOM, 防止office打开后中文乱码
- writer := csv.NewWriter(&content)
- writer.WriteAll(records)
- if err := writer.Error(); err != nil {
- log.Errorf("error writing csv:%s", err.Error())
- return content, err
- }
- return content, nil
- }
- func writeXlsx(sheetName string, records [][]string) (bytes.Buffer, error) {
- var content bytes.Buffer
- xlsx := excelize.NewFile()
- xlsx.SetSheetName("Sheet1", sheetName)
- index := xlsx.GetSheetIndex(sheetName)
- for i, record := range records {
- xlsx.SetSheetRow(sheetName, fmt.Sprintf("A%d", i+1), &record)
- }
- xlsx.SetActiveSheet(index)
- if err := xlsx.Write(&content); err != nil {
- log.Errorf("error writing xlsx:%s", err.Error())
- return content, err
- }
- return content, nil
- }
|