mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-05-19 17:03:21 +08:00
484 lines
14 KiB
Go
484 lines
14 KiB
Go
// Copyright 2019 Yunion
|
||
//
|
||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||
// you may not use this file except in compliance with the License.
|
||
// You may obtain a copy of the License at
|
||
//
|
||
// http://www.apache.org/licenses/LICENSE-2.0
|
||
//
|
||
// Unless required by applicable law or agreed to in writing, software
|
||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
// See the License for the specific language governing permissions and
|
||
// limitations under the License.
|
||
|
||
package handler
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"encoding/csv"
|
||
"fmt"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/360EntSecGroup-Skylar/excelize"
|
||
"golang.org/x/sync/errgroup"
|
||
|
||
"yunion.io/x/jsonutils"
|
||
"yunion.io/x/log"
|
||
|
||
"yunion.io/x/onecloud/pkg/apigateway/options"
|
||
"yunion.io/x/onecloud/pkg/appctx"
|
||
"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"
|
||
)
|
||
|
||
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地址"
|
||
)
|
||
|
||
const (
|
||
BATCH_USER_REGISTER_QUANTITY_LIMITATION = 1000
|
||
BATCH_HOST_REGISTER_QUANTITY_LIMITATION = 1000
|
||
)
|
||
|
||
func FetchSession(ctx context.Context, r *http.Request, apiVersion string) *mcclient.ClientSession {
|
||
token := AppContextToken(ctx)
|
||
session := auth.GetSession(ctx, token, FetchRegion(r), apiVersion)
|
||
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))
|
||
app.AddHandler(POST, prefix+"piuploads", FetchAuthToken(h.postPIUploads)) // itsm process instances upload api
|
||
}
|
||
|
||
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(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(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(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(w, e)
|
||
return
|
||
}
|
||
|
||
fileHeader := hostfiles[0].Header
|
||
contentType := fileHeader.Get("Content-Type")
|
||
if contentType != "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" {
|
||
e := httperrors.NewInputParameterError("Wrong content type %s, required application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", contentType)
|
||
httperrors.JsonClientError(w, e)
|
||
return
|
||
}
|
||
|
||
file, err := hostfiles[0].Open()
|
||
defer file.Close()
|
||
if err != nil {
|
||
log.Errorf(err.Error())
|
||
e := httperrors.NewInternalServerError("can't open file")
|
||
httperrors.JsonClientError(w, e)
|
||
return
|
||
}
|
||
|
||
xlsx, err := excelize.OpenReader(file)
|
||
if err != nil {
|
||
log.Errorf(err.Error())
|
||
e := httperrors.NewInternalServerError("can't parse file")
|
||
httperrors.JsonClientError(w, e)
|
||
return
|
||
}
|
||
|
||
rows := xlsx.GetRows("hosts")
|
||
if len(rows) == 0 {
|
||
e := httperrors.NewGeneralError(fmt.Errorf("empty file content"))
|
||
httperrors.JsonClientError(w, e)
|
||
return
|
||
}
|
||
|
||
paramKeys := []string{}
|
||
for _, title := range rows[0] {
|
||
switch title {
|
||
case HOST_MAC:
|
||
paramKeys = append(paramKeys, "access_mac")
|
||
case HOST_NAME:
|
||
paramKeys = append(paramKeys, "name")
|
||
case HOST_IPMI_ADDR, HOST_IPMI_ADDR_OPTIONAL:
|
||
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:
|
||
paramKeys = append(paramKeys, "access_ip")
|
||
default:
|
||
e := httperrors.NewInternalServerError("empty file content")
|
||
httperrors.JsonClientError(w, e)
|
||
return
|
||
}
|
||
}
|
||
|
||
// skipped header row
|
||
if len(rows) > BATCH_HOST_REGISTER_QUANTITY_LIMITATION {
|
||
e := httperrors.NewInputParameterError(fmt.Sprintf("beyond limitation. excel file rows must less than %d", BATCH_HOST_REGISTER_QUANTITY_LIMITATION))
|
||
httperrors.JsonClientError(w, e)
|
||
return
|
||
}
|
||
|
||
hosts := bytes.Buffer{}
|
||
for _, row := range rows[1:] {
|
||
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 := modules.Hosts.BatchRegister(s, paramKeys, params)
|
||
if err != nil {
|
||
e := httperrors.NewGeneralError(err)
|
||
httperrors.JsonClientError(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(w, e)
|
||
return
|
||
}
|
||
|
||
fileHeader := userfiles[0].Header
|
||
contentType := fileHeader.Get("Content-Type")
|
||
if contentType != "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" {
|
||
e := httperrors.NewInputParameterError("Wrong content type %s, required application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", contentType)
|
||
httperrors.JsonClientError(w, e)
|
||
return
|
||
}
|
||
|
||
file, err := userfiles[0].Open()
|
||
defer file.Close()
|
||
if err != nil {
|
||
log.Errorf(err.Error())
|
||
e := httperrors.NewInternalServerError("can't open file")
|
||
httperrors.JsonClientError(w, e)
|
||
return
|
||
}
|
||
|
||
xlsx, err := excelize.OpenReader(file)
|
||
if err != nil {
|
||
log.Errorf(err.Error())
|
||
e := httperrors.NewInternalServerError("can't parse file")
|
||
httperrors.JsonClientError(w, e)
|
||
return
|
||
}
|
||
|
||
// skipped header row
|
||
rows := xlsx.GetRows("users")
|
||
if len(rows) <= 1 {
|
||
e := httperrors.NewInputParameterError("empty file")
|
||
httperrors.JsonClientError(w, e)
|
||
return
|
||
} else if len(rows) > BATCH_USER_REGISTER_QUANTITY_LIMITATION {
|
||
e := httperrors.NewInputParameterError(fmt.Sprintf("beyond limitation.excel file rows must less than %d", BATCH_USER_REGISTER_QUANTITY_LIMITATION))
|
||
httperrors.JsonClientError(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]
|
||
domain := row[2]
|
||
allowWebConsole := strings.ToLower(row[3])
|
||
|
||
// 忽略空白行
|
||
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(w, e)
|
||
return
|
||
}
|
||
|
||
if _, ok := names[name]; ok {
|
||
e := httperrors.NewClientError("row %d duplicate name %s", rowIdx, name)
|
||
httperrors.JsonClientError(w, e)
|
||
return
|
||
} else {
|
||
names[name] = true
|
||
_, err := modules.UsersV3.Get(s, name, nil)
|
||
if err == nil {
|
||
continue
|
||
}
|
||
}
|
||
|
||
domainId, ok := domains[domain]
|
||
if !ok {
|
||
if len(domain) == 0 {
|
||
e := httperrors.NewClientError("row %d domain is empty", rowIdx)
|
||
httperrors.JsonClientError(w, e)
|
||
return
|
||
}
|
||
|
||
id, err := modules.Domains.GetId(adminS, domain, nil)
|
||
if err != nil {
|
||
httperrors.JsonClientError(w, httperrors.NewGeneralError(err))
|
||
return
|
||
}
|
||
|
||
domainId = id
|
||
domains[domain] = id
|
||
}
|
||
|
||
user := jsonutils.NewDict()
|
||
user.Add(jsonutils.NewString(name), "name")
|
||
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 := modules.UsersV3.Create(s, user)
|
||
return err
|
||
})
|
||
}
|
||
|
||
if err := userG.Wait(); err != nil {
|
||
e := httperrors.NewGeneralError(err)
|
||
httperrors.GeneralServerError(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.InvalidInputError(w, "not found")
|
||
return
|
||
}
|
||
|
||
var err error
|
||
var content bytes.Buffer
|
||
switch template {
|
||
case "BatchHostRegister":
|
||
records := [][]string{{HOST_MAC, HOST_NAME, HOST_IPMI_ADDR_OPTIONAL, HOST_IPMI_USERNAME_OPTIONAL, HOST_IPMI_PASSWORD_OPTIONAL}}
|
||
content, err = writeXlsx("hosts", records)
|
||
if err != nil {
|
||
httperrors.InternalServerError(w, "internal server error")
|
||
return
|
||
}
|
||
case "BatchHostISORegister":
|
||
records := [][]string{{HOST_NAME, HOST_IPMI_ADDR, HOST_IPMI_USERNAME, HOST_IPMI_PASSWORD, HOST_MNG_IP_ADDR}}
|
||
content, err = writeXlsx("hosts", records)
|
||
if err != nil {
|
||
httperrors.InternalServerError(w, "internal server error")
|
||
return
|
||
}
|
||
case "BatchHostPXERegister":
|
||
records := [][]string{{HOST_NAME, HOST_IPMI_ADDR, HOST_IPMI_USERNAME, HOST_IPMI_PASSWORD, HOST_MNG_IP_ADDR_OPTIONAL}}
|
||
content, err = writeXlsx("hosts", records)
|
||
if err != nil {
|
||
httperrors.InternalServerError(w, "internal server error")
|
||
return
|
||
}
|
||
case "BatchUserRegister":
|
||
records := [][]string{{"*用户名(user)", "用户密码(password)", "*部门/域(domain)", "*是否登录控制台(allow_web_console:true、false)"}}
|
||
content, err = writeXlsx("users", records)
|
||
if err != nil {
|
||
httperrors.InternalServerError(w, "internal server error")
|
||
return
|
||
}
|
||
case "BatchProjectRegister":
|
||
var titles []string
|
||
if options.Options.NonDefaultDomainProjects {
|
||
titles = []string{"项目名称", "域", "配额"}
|
||
} else {
|
||
titles = []string{"项目名称", "域"}
|
||
}
|
||
|
||
records := [][]string{titles}
|
||
content, err = writeXlsx("projects", records)
|
||
if err != nil {
|
||
httperrors.InternalServerError(w, "internal server error")
|
||
return
|
||
}
|
||
default:
|
||
httperrors.InputParameterError(w, "template not found %s", template)
|
||
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) postPIUploads(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
||
// 5 MB
|
||
var maxMemory int64 = 5 << 20
|
||
if req.ContentLength > maxMemory {
|
||
httperrors.InvalidInputError(w, "request body is too large.")
|
||
return
|
||
}
|
||
|
||
if !strings.Contains(req.Header.Get("Content-Type"), "multipart/form-data") {
|
||
httperrors.InvalidInputError(w, "invalid multipart form")
|
||
return
|
||
}
|
||
|
||
s := FetchSession(ctx, req, "")
|
||
header := http.Header{}
|
||
header.Set("Content-Type", req.Header.Get("Content-Type"))
|
||
header.Set("Content-Length", req.Header.Get("Content-Length"))
|
||
resp, err := modules.ProcessInstance.Upload(s, header, req.Body)
|
||
if err != nil {
|
||
httperrors.GeneralServerError(w, err)
|
||
return
|
||
}
|
||
|
||
appsrv.SendJSON(w, resp)
|
||
}
|
||
|
||
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
|
||
}
|