mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 12:40:25 +08:00
507 lines
16 KiB
Go
507 lines
16 KiB
Go
package plugins
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
|
|
"github.com/goravel/framework/contracts/http"
|
|
"github.com/spf13/cast"
|
|
|
|
"github.com/TheTNB/panel/v2/app/models"
|
|
"github.com/TheTNB/panel/v2/internal"
|
|
"github.com/TheTNB/panel/v2/internal/services"
|
|
"github.com/TheTNB/panel/v2/pkg/db"
|
|
"github.com/TheTNB/panel/v2/pkg/h"
|
|
"github.com/TheTNB/panel/v2/pkg/io"
|
|
"github.com/TheTNB/panel/v2/pkg/shell"
|
|
"github.com/TheTNB/panel/v2/pkg/str"
|
|
"github.com/TheTNB/panel/v2/pkg/systemctl"
|
|
"github.com/TheTNB/panel/v2/pkg/types"
|
|
)
|
|
|
|
type MySQLController struct {
|
|
setting internal.Setting
|
|
backup internal.Backup
|
|
}
|
|
|
|
func NewMySQLController() *MySQLController {
|
|
return &MySQLController{
|
|
setting: services.NewSettingImpl(),
|
|
backup: services.NewBackupImpl(),
|
|
}
|
|
}
|
|
|
|
// GetConfig 获取配置
|
|
func (r *MySQLController) GetConfig(ctx http.Context) http.Response {
|
|
config, err := io.Read("/www/server/mysql/conf/my.cnf")
|
|
if err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, "获取MySQL配置失败")
|
|
}
|
|
|
|
return h.Success(ctx, config)
|
|
}
|
|
|
|
// SaveConfig 保存配置
|
|
func (r *MySQLController) SaveConfig(ctx http.Context) http.Response {
|
|
config := ctx.Request().Input("config")
|
|
if len(config) == 0 {
|
|
return h.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空")
|
|
}
|
|
|
|
if err := io.Write("/www/server/mysql/conf/my.cnf", config, 0644); err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, "写入MySQL配置失败")
|
|
}
|
|
|
|
if err := systemctl.Reload("mysqld"); err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, "重载MySQL失败")
|
|
}
|
|
|
|
return h.Success(ctx, nil)
|
|
}
|
|
|
|
// Load 获取负载
|
|
func (r *MySQLController) Load(ctx http.Context) http.Response {
|
|
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
|
if len(rootPassword) == 0 {
|
|
return h.Error(ctx, http.StatusUnprocessableEntity, "MySQL root密码为空")
|
|
}
|
|
|
|
status, _ := systemctl.Status("mysqld")
|
|
if !status {
|
|
return h.Success(ctx, []types.NV{})
|
|
}
|
|
|
|
raw, err := shell.Execf("mysqladmin -uroot -p" + rootPassword + " extended-status 2>&1")
|
|
if err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, "获取MySQL负载失败")
|
|
}
|
|
|
|
var data []map[string]string
|
|
expressions := []struct {
|
|
regex string
|
|
name string
|
|
}{
|
|
{`Uptime\s+\|\s+(\d+)\s+\|`, "运行时间"},
|
|
{`Queries\s+\|\s+(\d+)\s+\|`, "总查询次数"},
|
|
{`Connections\s+\|\s+(\d+)\s+\|`, "总连接次数"},
|
|
{`Com_commit\s+\|\s+(\d+)\s+\|`, "每秒事务"},
|
|
{`Com_rollback\s+\|\s+(\d+)\s+\|`, "每秒回滚"},
|
|
{`Bytes_sent\s+\|\s+(\d+)\s+\|`, "发送"},
|
|
{`Bytes_received\s+\|\s+(\d+)\s+\|`, "接收"},
|
|
{`Threads_connected\s+\|\s+(\d+)\s+\|`, "活动连接数"},
|
|
{`Max_used_connections\s+\|\s+(\d+)\s+\|`, "峰值连接数"},
|
|
{`Key_read_requests\s+\|\s+(\d+)\s+\|`, "索引命中率"},
|
|
{`Innodb_buffer_pool_reads\s+\|\s+(\d+)\s+\|`, "Innodb索引命中率"},
|
|
{`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, "创建临时表到磁盘"},
|
|
{`Open_tables\s+\|\s+(\d+)\s+\|`, "已打开的表"},
|
|
{`Select_full_join\s+\|\s+(\d+)\s+\|`, "没有使用索引的量"},
|
|
{`Select_full_range_join\s+\|\s+(\d+)\s+\|`, "没有索引的JOIN量"},
|
|
{`Select_range_check\s+\|\s+(\d+)\s+\|`, "没有索引的子查询量"},
|
|
{`Sort_merge_passes\s+\|\s+(\d+)\s+\|`, "排序后的合并次数"},
|
|
{`Table_locks_waited\s+\|\s+(\d+)\s+\|`, "锁表次数"},
|
|
}
|
|
|
|
for _, expression := range expressions {
|
|
re := regexp.MustCompile(expression.regex)
|
|
matches := re.FindStringSubmatch(raw)
|
|
if len(matches) > 1 {
|
|
d := map[string]string{"name": expression.name, "value": matches[1]}
|
|
if expression.name == "发送" || expression.name == "接收" {
|
|
d["value"] = str.FormatBytes(cast.ToFloat64(matches[1]))
|
|
}
|
|
|
|
data = append(data, d)
|
|
}
|
|
}
|
|
|
|
// 索引命中率
|
|
readRequests := cast.ToFloat64(data[9]["value"])
|
|
reads := cast.ToFloat64(data[10]["value"])
|
|
data[9]["value"] = fmt.Sprintf("%.2f%%", readRequests/(reads+readRequests)*100)
|
|
// Innodb 索引命中率
|
|
bufferPoolReads := cast.ToFloat64(data[11]["value"])
|
|
bufferPoolReadRequests := cast.ToFloat64(data[12]["value"])
|
|
data[10]["value"] = fmt.Sprintf("%.2f%%", bufferPoolReadRequests/(bufferPoolReads+bufferPoolReadRequests)*100)
|
|
|
|
return h.Success(ctx, data)
|
|
}
|
|
|
|
// ErrorLog 获取错误日志
|
|
func (r *MySQLController) ErrorLog(ctx http.Context) http.Response {
|
|
log, err := shell.Execf("tail -n 100 /www/server/mysql/mysql-error.log")
|
|
if err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, log)
|
|
}
|
|
|
|
return h.Success(ctx, log)
|
|
}
|
|
|
|
// ClearErrorLog 清空错误日志
|
|
func (r *MySQLController) ClearErrorLog(ctx http.Context) http.Response {
|
|
if out, err := shell.Execf("echo '' > /www/server/mysql/mysql-error.log"); err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, out)
|
|
}
|
|
|
|
return h.Success(ctx, nil)
|
|
}
|
|
|
|
// SlowLog 获取慢查询日志
|
|
func (r *MySQLController) SlowLog(ctx http.Context) http.Response {
|
|
log, err := shell.Execf("tail -n 100 /www/server/mysql/mysql-slow.log")
|
|
if err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, log)
|
|
}
|
|
|
|
return h.Success(ctx, log)
|
|
}
|
|
|
|
// ClearSlowLog 清空慢查询日志
|
|
func (r *MySQLController) ClearSlowLog(ctx http.Context) http.Response {
|
|
if out, err := shell.Execf("echo '' > /www/server/mysql/mysql-slow.log"); err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, out)
|
|
}
|
|
return h.Success(ctx, nil)
|
|
}
|
|
|
|
// GetRootPassword 获取root密码
|
|
func (r *MySQLController) GetRootPassword(ctx http.Context) http.Response {
|
|
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
|
if len(rootPassword) == 0 {
|
|
return h.Error(ctx, http.StatusUnprocessableEntity, "MySQL root密码为空")
|
|
}
|
|
|
|
return h.Success(ctx, rootPassword)
|
|
}
|
|
|
|
// SetRootPassword 设置root密码
|
|
func (r *MySQLController) SetRootPassword(ctx http.Context) http.Response {
|
|
rootPassword := ctx.Request().Input("password")
|
|
if len(rootPassword) == 0 {
|
|
return h.Error(ctx, http.StatusUnprocessableEntity, "MySQL root密码不能为空")
|
|
}
|
|
|
|
oldRootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
|
mysql, err := db.NewMySQL("root", oldRootPassword, r.getSock(), "unix")
|
|
if err != nil {
|
|
// 尝试安全模式直接改密
|
|
if err = db.MySQLResetRootPassword(rootPassword); err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, err.Error())
|
|
}
|
|
} else {
|
|
if err = mysql.UserPassword("root", rootPassword); err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, err.Error())
|
|
}
|
|
}
|
|
if err = r.setting.Set(models.SettingKeyMysqlRootPassword, rootPassword); err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("设置保存失败: %v", err))
|
|
}
|
|
|
|
return h.Success(ctx, nil)
|
|
}
|
|
|
|
// DatabaseList 获取数据库列表
|
|
func (r *MySQLController) DatabaseList(ctx http.Context) http.Response {
|
|
password := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
|
mysql, err := db.NewMySQL("root", password, r.getSock(), "unix")
|
|
if err != nil {
|
|
return h.Success(ctx, http.Json{
|
|
"total": 0,
|
|
"items": []types.MySQLDatabase{},
|
|
})
|
|
}
|
|
|
|
databases, err := mysql.Databases()
|
|
if err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
|
|
}
|
|
paged, total := h.Paginate(ctx, databases)
|
|
|
|
return h.Success(ctx, http.Json{
|
|
"total": total,
|
|
"items": paged,
|
|
})
|
|
}
|
|
|
|
// AddDatabase 添加数据库
|
|
func (r *MySQLController) AddDatabase(ctx http.Context) http.Response {
|
|
if sanitize := h.Sanitize(ctx, map[string]string{
|
|
"database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$",
|
|
"user": "required|min_len:1|max_len:32|regex:^[a-zA-Z0-9_]+$",
|
|
"password": "required|min_len:8|max_len:32",
|
|
}); sanitize != nil {
|
|
return sanitize
|
|
}
|
|
|
|
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
|
database := ctx.Request().Input("database")
|
|
user := ctx.Request().Input("user")
|
|
password := ctx.Request().Input("password")
|
|
|
|
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
|
|
if err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, err.Error())
|
|
}
|
|
if err = mysql.DatabaseCreate(database); err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, err.Error())
|
|
}
|
|
if err = mysql.UserCreate(user, password); err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, err.Error())
|
|
}
|
|
if err = mysql.PrivilegesGrant(user, database); err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
return h.Success(ctx, nil)
|
|
}
|
|
|
|
// DeleteDatabase 删除数据库
|
|
func (r *MySQLController) DeleteDatabase(ctx http.Context) http.Response {
|
|
if sanitize := h.Sanitize(ctx, map[string]string{
|
|
"database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
|
|
}); sanitize != nil {
|
|
return sanitize
|
|
}
|
|
|
|
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
|
database := ctx.Request().Input("database")
|
|
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
|
|
if err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, err.Error())
|
|
}
|
|
if err = mysql.DatabaseDrop(database); err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
return h.Success(ctx, nil)
|
|
}
|
|
|
|
// BackupList 获取备份列表
|
|
func (r *MySQLController) BackupList(ctx http.Context) http.Response {
|
|
backups, err := r.backup.MysqlList()
|
|
if err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
paged, total := h.Paginate(ctx, backups)
|
|
|
|
return h.Success(ctx, http.Json{
|
|
"total": total,
|
|
"items": paged,
|
|
})
|
|
}
|
|
|
|
// UploadBackup 上传备份
|
|
func (r *MySQLController) UploadBackup(ctx http.Context) http.Response {
|
|
file, err := ctx.Request().File("file")
|
|
if err != nil {
|
|
return h.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败")
|
|
}
|
|
|
|
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/mysql"
|
|
if !io.Exists(backupPath) {
|
|
if err = io.Mkdir(backupPath, 0644); err != nil {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
name := file.GetClientOriginalName()
|
|
_, err = file.StoreAs(backupPath, name)
|
|
if err != nil {
|
|
return h.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败")
|
|
}
|
|
|
|
return h.Success(ctx, nil)
|
|
}
|
|
|
|
// CreateBackup 创建备份
|
|
func (r *MySQLController) CreateBackup(ctx http.Context) http.Response {
|
|
if sanitize := h.Sanitize(ctx, map[string]string{
|
|
"database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
|
|
}); sanitize != nil {
|
|
return sanitize
|
|
}
|
|
|
|
database := ctx.Request().Input("database")
|
|
if err := r.backup.MysqlBackup(database); err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
return h.Success(ctx, nil)
|
|
}
|
|
|
|
// DeleteBackup 删除备份
|
|
func (r *MySQLController) DeleteBackup(ctx http.Context) http.Response {
|
|
if sanitize := h.Sanitize(ctx, map[string]string{
|
|
"name": "required|min_len:1|max_len:255",
|
|
}); sanitize != nil {
|
|
return sanitize
|
|
}
|
|
|
|
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/mysql"
|
|
fileName := ctx.Request().Input("name")
|
|
if err := io.Remove(backupPath + "/" + fileName); err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
return h.Success(ctx, nil)
|
|
}
|
|
|
|
// RestoreBackup 还原备份
|
|
func (r *MySQLController) RestoreBackup(ctx http.Context) http.Response {
|
|
if sanitize := h.Sanitize(ctx, map[string]string{
|
|
"backup": "required|min_len:1|max_len:255",
|
|
"database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
|
|
}); sanitize != nil {
|
|
return sanitize
|
|
}
|
|
|
|
if err := r.backup.MysqlRestore(ctx.Request().Input("database"), ctx.Request().Input("backup")); err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
return h.Success(ctx, nil)
|
|
}
|
|
|
|
// UserList 用户列表
|
|
func (r *MySQLController) UserList(ctx http.Context) http.Response {
|
|
password := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
|
mysql, err := db.NewMySQL("root", password, r.getSock(), "unix")
|
|
if err != nil {
|
|
return h.Success(ctx, http.Json{
|
|
"total": 0,
|
|
"items": []types.MySQLUser{},
|
|
})
|
|
}
|
|
|
|
users, err := mysql.Users()
|
|
if err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, "获取用户列表失败")
|
|
}
|
|
paged, total := h.Paginate(ctx, users)
|
|
|
|
return h.Success(ctx, http.Json{
|
|
"total": total,
|
|
"items": paged,
|
|
})
|
|
}
|
|
|
|
// AddUser 添加用户
|
|
func (r *MySQLController) AddUser(ctx http.Context) http.Response {
|
|
if sanitize := h.Sanitize(ctx, map[string]string{
|
|
"database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$",
|
|
"user": "required|min_len:1|max_len:32|regex:^[a-zA-Z0-9_]+$",
|
|
"password": "required|min_len:8|max_len:32",
|
|
}); sanitize != nil {
|
|
return sanitize
|
|
}
|
|
|
|
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
|
user := ctx.Request().Input("user")
|
|
password := ctx.Request().Input("password")
|
|
database := ctx.Request().Input("database")
|
|
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
|
|
if err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, err.Error())
|
|
}
|
|
if err = mysql.UserCreate(user, password); err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, err.Error())
|
|
}
|
|
if err = mysql.PrivilegesGrant(user, database); err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
return h.Success(ctx, nil)
|
|
}
|
|
|
|
// DeleteUser 删除用户
|
|
func (r *MySQLController) DeleteUser(ctx http.Context) http.Response {
|
|
if sanitize := h.Sanitize(ctx, map[string]string{
|
|
"user": "required|min_len:1|max_len:32|regex:^[a-zA-Z0-9_]+$",
|
|
}); sanitize != nil {
|
|
return sanitize
|
|
}
|
|
|
|
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
|
user := ctx.Request().Input("user")
|
|
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
|
|
if err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, err.Error())
|
|
}
|
|
if err = mysql.UserDrop(user); err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
return h.Success(ctx, nil)
|
|
}
|
|
|
|
// SetUserPassword 设置用户密码
|
|
func (r *MySQLController) SetUserPassword(ctx http.Context) http.Response {
|
|
if sanitize := h.Sanitize(ctx, map[string]string{
|
|
"user": "required|min_len:1|max_len:32|regex:^[a-zA-Z0-9_]+$",
|
|
"password": "required|min_len:8|max_len:32",
|
|
}); sanitize != nil {
|
|
return sanitize
|
|
}
|
|
|
|
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
|
user := ctx.Request().Input("user")
|
|
password := ctx.Request().Input("password")
|
|
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
|
|
if err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, err.Error())
|
|
}
|
|
if err = mysql.UserPassword(user, password); err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
return h.Success(ctx, nil)
|
|
}
|
|
|
|
// SetUserPrivileges 设置用户权限
|
|
func (r *MySQLController) SetUserPrivileges(ctx http.Context) http.Response {
|
|
if sanitize := h.Sanitize(ctx, map[string]string{
|
|
"user": "required|min_len:1|max_len:32|regex:^[a-zA-Z0-9_]+$",
|
|
"database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$",
|
|
}); sanitize != nil {
|
|
return sanitize
|
|
}
|
|
|
|
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
|
user := ctx.Request().Input("user")
|
|
database := ctx.Request().Input("database")
|
|
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
|
|
if err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, err.Error())
|
|
}
|
|
if err = mysql.PrivilegesGrant(user, database); err != nil {
|
|
return h.Error(ctx, http.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
return h.Success(ctx, nil)
|
|
}
|
|
|
|
// getSock 获取sock文件位置
|
|
func (r *MySQLController) getSock() string {
|
|
if io.Exists("/tmp/mysql.sock") {
|
|
return "/tmp/mysql.sock"
|
|
}
|
|
if io.Exists("/www/server/mysql/config/my.cnf") {
|
|
config, _ := io.Read("/www/server/mysql/config/my.cnf")
|
|
re := regexp.MustCompile(`socket\s*=\s*(['"]?)([^'"]+)`)
|
|
matches := re.FindStringSubmatch(config)
|
|
if len(matches) > 2 {
|
|
return matches[2]
|
|
}
|
|
}
|
|
if io.Exists("/etc/my.cnf") {
|
|
config, _ := io.Read("/etc/my.cnf")
|
|
re := regexp.MustCompile(`socket\s*=\s*(['"]?)([^'"]+)`)
|
|
matches := re.FindStringSubmatch(config)
|
|
if len(matches) > 2 {
|
|
return matches[2]
|
|
}
|
|
}
|
|
|
|
return "/tmp/mysql.sock"
|
|
}
|