Files
cloudpods/pkg/apigateway/handler/misc.go
2020-01-10 17:10:49 +08:00

484 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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_consoletrue、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
}