mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-01-11 09:40:17 +08:00
fix: Modify the terminal connection method (#8415)
This commit is contained in:
parent
779260145c
commit
5a1e010788
26 changed files with 328 additions and 105 deletions
|
|
@ -1,10 +1,17 @@
|
|||
package v2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ssh"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// @Tags System Setting
|
||||
|
|
@ -63,3 +70,75 @@ func (b *BaseApi) UpdateSetting(c *gin.Context) {
|
|||
func (b *BaseApi) LoadBaseDir(c *gin.Context) {
|
||||
helper.SuccessWithData(c, global.Dir.DataDir)
|
||||
}
|
||||
|
||||
func (b *BaseApi) CheckLocalConn(c *gin.Context) {
|
||||
_, err := loadLocalConn()
|
||||
helper.SuccessWithData(c, err == nil)
|
||||
}
|
||||
|
||||
// @Tags System Setting
|
||||
// @Summary Check local conn info
|
||||
// @Success 200 {bool} isOk
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /settings/ssh/check/info [post]
|
||||
func (b *BaseApi) CheckLocalConnByInfo(c *gin.Context) {
|
||||
var req dto.SSHConnData
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, settingService.TestConnByInfo(req))
|
||||
}
|
||||
|
||||
// @Tags System Setting
|
||||
// @Summary Save local conn info
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /settings/ssh [post]
|
||||
func (b *BaseApi) SaveLocalConnInfo(c *gin.Context) {
|
||||
var req dto.SSHConnData
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, settingService.SaveConnInfo(req))
|
||||
}
|
||||
|
||||
func loadLocalConn() (*ssh.SSHClient, error) {
|
||||
itemPath := ""
|
||||
currentInfo, _ := user.Current()
|
||||
if len(currentInfo.HomeDir) == 0 {
|
||||
itemPath = "/root/.ssh/id_ed25519_1panel"
|
||||
} else {
|
||||
itemPath = path.Join(currentInfo.HomeDir, ".ssh/id_ed25519_1panel")
|
||||
}
|
||||
if _, err := os.Stat(itemPath); err != nil {
|
||||
_ = sshService.GenerateSSH(dto.GenerateSSH{EncryptionMode: "ed25519", Name: "_1panel"})
|
||||
}
|
||||
|
||||
privateKey, _ := os.ReadFile(itemPath)
|
||||
connWithKey := ssh.ConnInfo{
|
||||
Addr: "127.0.0.1",
|
||||
User: "root",
|
||||
Port: 22,
|
||||
AuthMode: "key",
|
||||
PrivateKey: privateKey,
|
||||
}
|
||||
client, err := ssh.NewClient(connWithKey)
|
||||
if err == nil {
|
||||
return client, nil
|
||||
}
|
||||
|
||||
connInfoInDB, err := settingService.GetSSHInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(connInfoInDB) == 0 {
|
||||
return nil, errors.New("no such ssh conn info in db!")
|
||||
}
|
||||
var connInDB ssh.ConnInfo
|
||||
if err := json.Unmarshal([]byte(connInfoInDB), &connInDB); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ssh.NewClient(connInDB)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,24 +40,21 @@ func (b *BaseApi) WsSSH(c *gin.Context) {
|
|||
if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) {
|
||||
return
|
||||
}
|
||||
name, err := loadExecutor()
|
||||
if wshandleError(wsConn, err) {
|
||||
return
|
||||
}
|
||||
slave, err := terminal.NewCommand(name)
|
||||
if wshandleError(wsConn, err) {
|
||||
return
|
||||
}
|
||||
defer slave.Close()
|
||||
|
||||
tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false)
|
||||
client, err := loadLocalConn()
|
||||
if wshandleError(wsConn, errors.WithMessage(err, "failed to set up the connection. Please check the host information")) {
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
sws, err := terminal.NewLogicSshWsSession(cols, rows, client.Client, wsConn, "")
|
||||
if wshandleError(wsConn, err) {
|
||||
return
|
||||
}
|
||||
defer sws.Close()
|
||||
|
||||
quitChan := make(chan bool, 3)
|
||||
tty.Start(quitChan)
|
||||
go slave.Wait(quitChan)
|
||||
sws.Start(quitChan)
|
||||
go sws.Wait(quitChan)
|
||||
|
||||
<-quitChan
|
||||
|
||||
|
|
@ -254,12 +251,3 @@ var upGrader = websocket.Upgrader{
|
|||
return true
|
||||
},
|
||||
}
|
||||
|
||||
func loadExecutor() (string, error) {
|
||||
std, err := cmd.RunDefaultWithStdoutBashC("echo $SHELL")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("load default executor failed, err: %s", std)
|
||||
}
|
||||
|
||||
return strings.ReplaceAll(std, "\n", ""), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,3 +61,13 @@ type Clean struct {
|
|||
Name string `json:"name"`
|
||||
Size uint64 `json:"size"`
|
||||
}
|
||||
|
||||
type SSHConnData struct {
|
||||
Addr string `json:"addr" validate:"required"`
|
||||
Port uint `json:"port" validate:"required,number,max=65535,min=1"`
|
||||
User string `json:"user" validate:"required"`
|
||||
AuthMode string `json:"authMode" validate:"oneof=password key"`
|
||||
Password string `json:"password"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
PassPhrase string `json:"passPhrase"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ type SSHInfo struct {
|
|||
type GenerateSSH struct {
|
||||
EncryptionMode string `json:"encryptionMode" validate:"required,oneof=rsa ed25519 ecdsa dsa"`
|
||||
Password string `json:"password"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type GenerateLoad struct {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ssh"
|
||||
"github.com/jinzhu/copier"
|
||||
)
|
||||
|
||||
type SettingService struct{}
|
||||
|
|
@ -13,6 +17,10 @@ type SettingService struct{}
|
|||
type ISettingService interface {
|
||||
GetSettingInfo() (*dto.SettingInfo, error)
|
||||
Update(key, value string) error
|
||||
|
||||
GetSSHInfo() (string, error)
|
||||
TestConnByInfo(req dto.SSHConnData) bool
|
||||
SaveConnInfo(req dto.SSHConnData) error
|
||||
}
|
||||
|
||||
func NewISettingService() ISettingService {
|
||||
|
|
@ -44,3 +52,75 @@ func (u *SettingService) GetSettingInfo() (*dto.SettingInfo, error) {
|
|||
func (u *SettingService) Update(key, value string) error {
|
||||
return settingRepo.UpdateOrCreate(key, value)
|
||||
}
|
||||
|
||||
func (u *SettingService) GetSSHInfo() (string, error) {
|
||||
conn, err := settingRepo.GetValueByKey("LocalSSHConn")
|
||||
if err != nil || len(conn) == 0 {
|
||||
return "", err
|
||||
}
|
||||
return encrypt.StringDecrypt(conn)
|
||||
}
|
||||
|
||||
func (u *SettingService) TestConnByInfo(req dto.SSHConnData) bool {
|
||||
if req.AuthMode == "password" && len(req.Password) != 0 {
|
||||
password, err := base64.StdEncoding.DecodeString(req.Password)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
req.Password = string(password)
|
||||
}
|
||||
if req.AuthMode == "key" && len(req.PrivateKey) != 0 {
|
||||
privateKey, err := base64.StdEncoding.DecodeString(req.PrivateKey)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
req.PrivateKey = string(privateKey)
|
||||
}
|
||||
|
||||
var connInfo ssh.ConnInfo
|
||||
_ = copier.Copy(&connInfo, &req)
|
||||
connInfo.PrivateKey = []byte(req.PrivateKey)
|
||||
if len(req.PassPhrase) != 0 {
|
||||
connInfo.PassPhrase = []byte(req.PassPhrase)
|
||||
}
|
||||
client, err := ssh.NewClient(connInfo)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer client.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
func (u *SettingService) SaveConnInfo(req dto.SSHConnData) error {
|
||||
if req.AuthMode == "password" && len(req.Password) != 0 {
|
||||
password, err := base64.StdEncoding.DecodeString(req.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Password = string(password)
|
||||
}
|
||||
if req.AuthMode == "key" && len(req.PrivateKey) != 0 {
|
||||
privateKey, err := base64.StdEncoding.DecodeString(req.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.PrivateKey = string(privateKey)
|
||||
}
|
||||
|
||||
var connInfo ssh.ConnInfo
|
||||
_ = copier.Copy(&connInfo, &req)
|
||||
connInfo.PrivateKey = []byte(req.PrivateKey)
|
||||
if len(req.PassPhrase) != 0 {
|
||||
connInfo.PassPhrase = []byte(req.PassPhrase)
|
||||
}
|
||||
client, err := ssh.NewClient(connInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
localConn, _ := json.Marshal(&connInfo)
|
||||
connAfterEncrypt, _ := encrypt.StringEncrypt(string(localConn))
|
||||
_ = settingRepo.Update("LocalSSHConn", connAfterEncrypt)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -254,10 +254,10 @@ func (u *SSHService) GenerateSSH(req dto.GenerateSSH) error {
|
|||
}
|
||||
|
||||
fileOp := files.NewFileOp()
|
||||
if err := fileOp.Rename(secretFile, fmt.Sprintf("%s/.ssh/id_%s", currentUser.HomeDir, req.EncryptionMode)); err != nil {
|
||||
if err := fileOp.Rename(secretFile, fmt.Sprintf("%s/.ssh/id_%s%s", currentUser.HomeDir, req.EncryptionMode, req.Name)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := fileOp.Rename(secretPubFile, fmt.Sprintf("%s/.ssh/id_%s.pub", currentUser.HomeDir, req.EncryptionMode)); err != nil {
|
||||
if err := fileOp.Rename(secretPubFile, fmt.Sprintf("%s/.ssh/id_%s%s.pub", currentUser.HomeDir, req.EncryptionMode, req.Name)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -114,13 +114,13 @@ func NewTask(name, operate, taskScope, taskID string, resourceID uint) (*Task, e
|
|||
if taskID == "" {
|
||||
taskID = uuid.New().String()
|
||||
}
|
||||
logDir := path.Join(global.Dir.LogDir, taskScope)
|
||||
logDir := path.Join(global.Dir.TaskDir, taskScope)
|
||||
if _, err := os.Stat(logDir); os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(logDir, constant.DirPerm); err != nil {
|
||||
return nil, fmt.Errorf("failed to create log directory: %w", err)
|
||||
}
|
||||
}
|
||||
logPath := path.Join(global.Dir.LogDir, taskScope, taskID+".log")
|
||||
logPath := path.Join(global.Dir.TaskDir, taskScope, taskID+".log")
|
||||
file, err := os.OpenFile(logPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, constant.FilePerm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open log file: %w", err)
|
||||
|
|
@ -267,7 +267,7 @@ func (t *Task) LogFailed(msg string) {
|
|||
}
|
||||
|
||||
func (t *Task) LogFailedWithErr(msg string, err error) {
|
||||
t.Logger.Println(fmt.Sprintf("%s %s : %s", msg, i18n.GetMsgByKey("Failed"), err.Error()))
|
||||
t.Logger.Printf("%s %s : %s\n", msg, i18n.GetMsgByKey("Failed"), err.Error())
|
||||
}
|
||||
|
||||
func (t *Task) LogSuccess(msg string) {
|
||||
|
|
@ -278,7 +278,7 @@ func (t *Task) LogSuccessF(format string, v ...any) {
|
|||
}
|
||||
|
||||
func (t *Task) LogStart(msg string) {
|
||||
t.Logger.Println(fmt.Sprintf("%s%s", i18n.GetMsgByKey("Start"), msg))
|
||||
t.Logger.Printf("%s%s\n", i18n.GetMsgByKey("Start"), msg)
|
||||
}
|
||||
|
||||
func (t *Task) LogWithOps(operate, msg string) {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ type SystemDir struct {
|
|||
BaseDir string
|
||||
DbDir string
|
||||
LogDir string
|
||||
TaskDir string
|
||||
DataDir string
|
||||
TmpDir string
|
||||
LocalBackupDir string
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ func Init() {
|
|||
global.Dir.BaseDir, _ = fileOp.CreateDirWithPath(true, baseDir)
|
||||
global.Dir.DataDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel"))
|
||||
global.Dir.DbDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/db"))
|
||||
global.Dir.LogDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/log/task"))
|
||||
global.Dir.LogDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/log"))
|
||||
global.Dir.TaskDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/log/task"))
|
||||
global.Dir.TmpDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/tmp"))
|
||||
|
||||
global.Dir.AppDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/apps"))
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ func InitAgentDB() {
|
|||
migrations.UpdateSettingStatus,
|
||||
migrations.InitDefault,
|
||||
migrations.UpdateWebsiteExpireDate,
|
||||
migrations.AddLocalSSHSetting,
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
global.LOG.Error(err)
|
||||
|
|
|
|||
|
|
@ -311,3 +311,13 @@ var UpdateWebsiteExpireDate = &gormigrate.Migration{
|
|||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var AddLocalSSHSetting = &gormigrate.Migration{
|
||||
ID: "20250417-add-local-ssh-setting",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
if err := tx.Create(&model.Setting{Key: "LocalSSHConn", Value: ""}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,6 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
|
|||
hostRouter.GET("/tool/supervisor/process", baseApi.GetProcess)
|
||||
hostRouter.POST("/tool/supervisor/process/file", baseApi.GetProcessFile)
|
||||
|
||||
hostRouter.GET("/exec", baseApi.WsSSH)
|
||||
hostRouter.GET("/terminal", baseApi.WsSSH)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,5 +26,9 @@ func (s *SettingRouter) InitRouter(Router *gin.RouterGroup) {
|
|||
settingRouter.POST("/snapshot/description/update", baseApi.UpdateSnapDescription)
|
||||
|
||||
settingRouter.GET("/basedir", baseApi.LoadBaseDir)
|
||||
|
||||
settingRouter.POST("/ssh/check", baseApi.CheckLocalConn)
|
||||
settingRouter.POST("/ssh", baseApi.SaveLocalConnInfo)
|
||||
settingRouter.POST("/ssh/check/info", baseApi.CheckLocalConnByInfo)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,13 +17,13 @@ type ConnInfo struct {
|
|||
PrivateKey []byte `json:"privateKey"`
|
||||
PassPhrase []byte `json:"passPhrase"`
|
||||
DialTimeOut time.Duration `json:"dialTimeOut"`
|
||||
|
||||
Client *gossh.Client `json:"client"`
|
||||
Session *gossh.Session `json:"session"`
|
||||
LastResult string `json:"lastResult"`
|
||||
}
|
||||
|
||||
func (c *ConnInfo) NewClient() (*ConnInfo, error) {
|
||||
type SSHClient struct {
|
||||
Client *gossh.Client `json:"client"`
|
||||
}
|
||||
|
||||
func NewClient(c ConnInfo) (*SSHClient, error) {
|
||||
if strings.Contains(c.Addr, ":") {
|
||||
c.Addr = fmt.Sprintf("[%s]", c.Addr)
|
||||
}
|
||||
|
|
@ -52,30 +52,12 @@ func (c *ConnInfo) NewClient() (*ConnInfo, error) {
|
|||
}
|
||||
client, err := gossh.Dial(proto, addr, config)
|
||||
if nil != err {
|
||||
return c, err
|
||||
return nil, err
|
||||
}
|
||||
c.Client = client
|
||||
return c, nil
|
||||
return &SSHClient{Client: client}, nil
|
||||
}
|
||||
|
||||
func (c *ConnInfo) Run(shell string) (string, error) {
|
||||
if c.Client == nil {
|
||||
if _, err := c.NewClient(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
session, err := c.Client.NewSession()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer session.Close()
|
||||
buf, err := session.CombinedOutput(shell)
|
||||
|
||||
c.LastResult = string(buf)
|
||||
return c.LastResult, err
|
||||
}
|
||||
|
||||
func (c *ConnInfo) Close() {
|
||||
func (c *SSHClient) Close() {
|
||||
_ = c.Client.Close()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ type LogicSshWsSession struct {
|
|||
IsFlagged bool
|
||||
}
|
||||
|
||||
func NewLogicSshWsSession(cols, rows int, isAdmin bool, sshClient *ssh.Client, wsConn *websocket.Conn) (*LogicSshWsSession, error) {
|
||||
func NewLogicSshWsSession(cols, rows int, sshClient *ssh.Client, wsConn *websocket.Conn, initCmd string) (*LogicSshWsSession, error) {
|
||||
sshSession, err := sshClient.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -87,6 +87,10 @@ func NewLogicSshWsSession(cols, rows int, isAdmin bool, sshClient *ssh.Client, w
|
|||
if err := sshSession.Shell(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(initCmd) != 0 {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
_, _ = stdinP.Write([]byte(initCmd + "\n"))
|
||||
}
|
||||
return &LogicSshWsSession{
|
||||
stdinPipe: stdinP,
|
||||
comboOutput: comboWriter,
|
||||
|
|
@ -94,7 +98,7 @@ func NewLogicSshWsSession(cols, rows int, isAdmin bool, sshClient *ssh.Client, w
|
|||
inputFilterBuff: inputBuf,
|
||||
session: sshSession,
|
||||
wsConn: wsConn,
|
||||
isAdmin: isAdmin,
|
||||
isAdmin: true,
|
||||
IsFlagged: false,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -110,6 +114,7 @@ func (sws *LogicSshWsSession) Close() {
|
|||
sws.comboOutput = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (sws *LogicSshWsSession) Start(quitChan chan bool) {
|
||||
go sws.receiveWsMsg(quitChan)
|
||||
go sws.sendComboOutput(quitChan)
|
||||
|
|
@ -148,7 +153,6 @@ func (sws *LogicSshWsSession) receiveWsMsg(exitCh chan bool) {
|
|||
}
|
||||
sws.sendWebsocketInputCommandToSshSessionStdinPipe(decodeBytes)
|
||||
case WsMsgHeartbeat:
|
||||
// 接收到心跳包后将心跳包原样返回,可以用于网络延迟检测等情况
|
||||
err = wsConn.WriteMessage(websocket.TextMessage, wsData)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("ssh sending heartbeat to webSocket failed, err: %v", err)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export namespace Host {
|
|||
description: string;
|
||||
}
|
||||
export interface HostOperate {
|
||||
isLocal: boolean;
|
||||
id: number;
|
||||
name: string;
|
||||
groupID: number;
|
||||
|
|
@ -40,6 +41,7 @@ export namespace Host {
|
|||
description: string;
|
||||
}
|
||||
export interface HostConnTest {
|
||||
isLocal: boolean;
|
||||
addr: string;
|
||||
port: number;
|
||||
user: string;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ export const addHost = (params: Host.HostOperate) => {
|
|||
if (request.privateKey) {
|
||||
request.privateKey = Base64.encode(request.privateKey);
|
||||
}
|
||||
if (params.isLocal) {
|
||||
return http.post(`/settings/ssh`, request);
|
||||
}
|
||||
return http.post<Host.HostOperate>(`/core/hosts`, request);
|
||||
};
|
||||
export const testByInfo = (params: Host.HostConnTest) => {
|
||||
|
|
@ -31,6 +34,9 @@ export const testByInfo = (params: Host.HostConnTest) => {
|
|||
if (request.privateKey) {
|
||||
request.privateKey = Base64.encode(request.privateKey);
|
||||
}
|
||||
if (params.isLocal) {
|
||||
return http.post<boolean>(`/settings/ssh/check/info`, request);
|
||||
}
|
||||
return http.post<boolean>(`/core/hosts/test/byinfo`, request);
|
||||
};
|
||||
export const testByID = (id: number) => {
|
||||
|
|
@ -52,3 +58,8 @@ export const editHostGroup = (params: Host.GroupChange) => {
|
|||
export const deleteHost = (params: { ids: number[] }) => {
|
||||
return http.post(`/core/hosts/del`, params);
|
||||
};
|
||||
|
||||
// agent
|
||||
export const testLocalConn = () => {
|
||||
return http.post<boolean>(`/settings/ssh/check`);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1092,6 +1092,7 @@ const message = {
|
|||
terminal: {
|
||||
local: 'Local',
|
||||
localHelper: 'The `local` name is used only for system local identification',
|
||||
connLocalErr: 'Unable to automatically authenticate, please fill in the local server login information!',
|
||||
testConn: 'Test connection',
|
||||
saveAndConn: 'Save and Connect',
|
||||
connTestOk: 'Connection information available',
|
||||
|
|
|
|||
|
|
@ -1050,6 +1050,7 @@ const message = {
|
|||
terminal: {
|
||||
local: 'ローカル',
|
||||
localHelper: 'ローカル名はシステムのローカル識別にのみ使用されます。',
|
||||
connLocalErr: '自動的に認証できない場合は、ローカルサーバーのログイン情報を入力してください。',
|
||||
testConn: 'テスト接続',
|
||||
saveAndConn: '保存して接続します',
|
||||
connTestOk: '利用可能な接続情報',
|
||||
|
|
|
|||
|
|
@ -1043,6 +1043,7 @@ const message = {
|
|||
terminal: {
|
||||
local: '로컬',
|
||||
localHelper: '로컬 이름은 시스템 로컬 식별에만 사용됩니다.',
|
||||
connLocalErr: '자동 인증에 실패했습니다. 로컬 서버 로그인 정보를 입력해주세요.',
|
||||
testConn: '연결 테스트',
|
||||
saveAndConn: '저장 후 연결',
|
||||
connTestOk: '연결 정보가 유효합니다.',
|
||||
|
|
|
|||
|
|
@ -1079,6 +1079,7 @@ const message = {
|
|||
terminal: {
|
||||
local: 'Tempatan',
|
||||
localHelper: 'Nama tempatan hanya digunakan untuk pengenalan sistem tempatan.',
|
||||
connLocalErr: 'Tidak dapat mengesahkan secara automatik, sila isi maklumat log masuk pelayan tempatan.',
|
||||
testConn: 'Uji sambungan',
|
||||
saveAndConn: 'Simpan dan sambung',
|
||||
connTestOk: 'Maklumat sambungan tersedia',
|
||||
|
|
|
|||
|
|
@ -1069,6 +1069,8 @@ const message = {
|
|||
terminal: {
|
||||
local: 'Local',
|
||||
localHelper: 'O nome local é usado apenas para identificação local do sistema.',
|
||||
connLocalErr:
|
||||
'Невозможно автоматически аутентифицироваться, пожалуйста, заполните информацию для входа на локальный сервер.',
|
||||
testConn: 'Testar conexão',
|
||||
saveAndConn: 'Salvar e conectar',
|
||||
connTestOk: 'Informações de conexão disponíveis',
|
||||
|
|
|
|||
|
|
@ -1074,6 +1074,7 @@ const message = {
|
|||
terminal: {
|
||||
local: 'Локальный',
|
||||
localHelper: 'Локальное имя используется только для локальной идентификации системы.',
|
||||
connLocalErr: '無法自動認證,請填寫本地服務器的登錄信息!',
|
||||
testConn: 'Проверить подключение',
|
||||
saveAndConn: 'Сохранить и подключиться',
|
||||
connTestOk: 'Информация о подключении доступна',
|
||||
|
|
|
|||
|
|
@ -1040,6 +1040,7 @@ const message = {
|
|||
terminal: {
|
||||
local: '本機',
|
||||
localHelper: 'local 名稱僅用於系統本機標識',
|
||||
connLocalErr: '无法自动认证,请填写本地服务器的登录信息!',
|
||||
testConn: '連接測試',
|
||||
saveAndConn: '保存並連接',
|
||||
connTestOk: '連接信息可用',
|
||||
|
|
|
|||
|
|
@ -1,41 +1,43 @@
|
|||
<template>
|
||||
<DrawerPro v-model="dialogVisible" :header="$t('terminal.addHost')" @close="handleClose" size="large">
|
||||
<el-form ref="hostRef" label-width="100px" label-position="top" :model="hostInfo" :rules="rules">
|
||||
<el-form ref="hostRef" label-width="100px" label-position="top" :model="form" :rules="rules">
|
||||
<el-alert
|
||||
v-if="form.isLocal"
|
||||
class="common-prompt"
|
||||
center
|
||||
:title="$t('terminal.connLocalErr')"
|
||||
:closable="false"
|
||||
type="warning"
|
||||
/>
|
||||
<el-form-item :label="$t('terminal.ip')" prop="addr">
|
||||
<el-input @change="isOK = false" clearable v-model.trim="hostInfo.addr" />
|
||||
<el-input @change="isOK = false" clearable v-model.trim="form.addr" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('commons.login.username')" prop="user">
|
||||
<el-input @change="isOK = false" clearable v-model="hostInfo.user" />
|
||||
<el-input @change="isOK = false" clearable v-model="form.user" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('terminal.authMode')" prop="authMode">
|
||||
<el-radio-group @change="isOK = false" v-model="hostInfo.authMode">
|
||||
<el-radio-group @change="isOK = false" v-model="form.authMode">
|
||||
<el-radio value="password">{{ $t('terminal.passwordMode') }}</el-radio>
|
||||
<el-radio value="key">{{ $t('terminal.keyMode') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('commons.login.password')" v-if="hostInfo.authMode === 'password'" prop="password">
|
||||
<el-input @change="isOK = false" clearable show-password type="password" v-model="hostInfo.password" />
|
||||
<el-form-item :label="$t('commons.login.password')" v-if="form.authMode === 'password'" prop="password">
|
||||
<el-input @change="isOK = false" clearable show-password type="password" v-model="form.password" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('terminal.key')" v-if="hostInfo.authMode === 'key'" prop="privateKey">
|
||||
<el-input @change="isOK = false" clearable type="textarea" v-model="hostInfo.privateKey" />
|
||||
<el-form-item :label="$t('terminal.key')" v-if="form.authMode === 'key'" prop="privateKey">
|
||||
<el-input @change="isOK = false" clearable type="textarea" v-model="form.privateKey" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('terminal.keyPassword')" v-if="hostInfo.authMode === 'key'" prop="passPhrase">
|
||||
<el-input
|
||||
@change="isOK = false"
|
||||
type="password"
|
||||
show-password
|
||||
clearable
|
||||
v-model="hostInfo.passPhrase"
|
||||
/>
|
||||
<el-form-item :label="$t('terminal.keyPassword')" v-if="form.authMode === 'key'" prop="passPhrase">
|
||||
<el-input @change="isOK = false" type="password" show-password clearable v-model="form.passPhrase" />
|
||||
</el-form-item>
|
||||
<el-checkbox clearable v-model.number="hostInfo.rememberPassword">
|
||||
<el-checkbox clearable v-model.number="form.rememberPassword">
|
||||
{{ $t('terminal.rememberPassword') }}
|
||||
</el-checkbox>
|
||||
<el-form-item class="mt-2.5" :label="$t('commons.table.port')" prop="port">
|
||||
<el-input @change="isOK = false" clearable v-model.number="hostInfo.port" />
|
||||
<el-input @change="isOK = false" clearable v-model.number="form.port" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('commons.table.group')" prop="groupID">
|
||||
<el-select filterable v-model="hostInfo.groupID" clearable style="width: 100%">
|
||||
<el-form-item v-if="!form.isLocal" :label="$t('commons.table.group')" prop="groupID">
|
||||
<el-select filterable v-model="form.groupID" clearable style="width: 100%">
|
||||
<div v-for="item in groupList" :key="item.id">
|
||||
<el-option
|
||||
v-if="item.name === 'Default'"
|
||||
|
|
@ -46,11 +48,11 @@
|
|||
</div>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('commons.table.title')" prop="name">
|
||||
<el-input clearable v-model="hostInfo.name" />
|
||||
<el-form-item v-if="!form.isLocal" :label="$t('commons.table.title')" prop="name">
|
||||
<el-input clearable v-model="form.name" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('commons.table.description')" prop="description">
|
||||
<el-input clearable v-model="hostInfo.description" />
|
||||
<el-form-item v-if="!form.isLocal" :label="$t('commons.table.description')" prop="description">
|
||||
<el-input clearable v-model="form.description" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
|
|
@ -71,7 +73,7 @@
|
|||
import { ElForm } from 'element-plus';
|
||||
import { Host } from '@/api/interface/host';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import { addHost, testByInfo } from '@/api/modules/terminal';
|
||||
import { addHost, editHost, testByInfo } from '@/api/modules/terminal';
|
||||
import i18n from '@/lang';
|
||||
import { reactive, ref } from 'vue';
|
||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||
|
|
@ -85,7 +87,7 @@ const hostRef = ref<FormInstance>();
|
|||
const groupList = ref();
|
||||
const defaultGroup = ref();
|
||||
|
||||
let hostInfo = reactive<Host.HostOperate>({
|
||||
let form = reactive<Host.HostOperate>({
|
||||
id: 0,
|
||||
name: '',
|
||||
groupID: 0,
|
||||
|
|
@ -98,6 +100,7 @@ let hostInfo = reactive<Host.HostOperate>({
|
|||
passPhrase: '',
|
||||
rememberPassword: false,
|
||||
description: '',
|
||||
isLocal: false,
|
||||
});
|
||||
|
||||
const rules = reactive({
|
||||
|
|
@ -109,7 +112,11 @@ const rules = reactive({
|
|||
privateKey: [Rules.requiredInput],
|
||||
});
|
||||
|
||||
const acceptParams = () => {
|
||||
interface DialogProps {
|
||||
isLocal: boolean;
|
||||
}
|
||||
const acceptParams = (props: DialogProps) => {
|
||||
form.isLocal = props.isLocal;
|
||||
loadGroups();
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
|
@ -118,7 +125,7 @@ const handleClose = () => {
|
|||
dialogVisible.value = false;
|
||||
};
|
||||
|
||||
const emit = defineEmits(['on-conn-terminal', 'load-host-tree']);
|
||||
const emit = defineEmits(['on-conn-terminal', 'on-new-local', 'load-host-tree']);
|
||||
|
||||
const loadGroups = async () => {
|
||||
const res = await getGroupList('host');
|
||||
|
|
@ -129,19 +136,32 @@ const loadGroups = async () => {
|
|||
break;
|
||||
}
|
||||
}
|
||||
setDefault();
|
||||
if (form.isLocal) {
|
||||
loadLocal();
|
||||
} else {
|
||||
setDefault();
|
||||
}
|
||||
};
|
||||
const loadLocal = async () => {
|
||||
form.id = 0;
|
||||
form.addr = '127.0.0.1';
|
||||
form.port = 22;
|
||||
form.user = 'root';
|
||||
form.authMode = 'password';
|
||||
form.password = '';
|
||||
form.privateKey = '';
|
||||
};
|
||||
|
||||
const setDefault = () => {
|
||||
hostInfo.addr = '';
|
||||
hostInfo.name = '';
|
||||
hostInfo.groupID = defaultGroup.value;
|
||||
hostInfo.port = 22;
|
||||
hostInfo.user = '';
|
||||
hostInfo.authMode = 'password';
|
||||
hostInfo.password = '';
|
||||
hostInfo.privateKey = '';
|
||||
hostInfo.description = '';
|
||||
form.addr = '';
|
||||
form.name = '';
|
||||
form.groupID = defaultGroup.value;
|
||||
form.port = 22;
|
||||
form.user = '';
|
||||
form.authMode = 'password';
|
||||
form.password = '';
|
||||
form.privateKey = '';
|
||||
form.description = '';
|
||||
};
|
||||
|
||||
const submitAddHost = (formEl: FormInstance | undefined, ops: string) => {
|
||||
|
|
@ -150,7 +170,7 @@ const submitAddHost = (formEl: FormInstance | undefined, ops: string) => {
|
|||
if (!valid) return;
|
||||
switch (ops) {
|
||||
case 'testConn':
|
||||
await testByInfo(hostInfo).then((res) => {
|
||||
await testByInfo(form).then((res) => {
|
||||
if (res.data) {
|
||||
isOK.value = true;
|
||||
MsgSuccess(i18n.global.t('terminal.connTestOk'));
|
||||
|
|
@ -161,8 +181,18 @@ const submitAddHost = (formEl: FormInstance | undefined, ops: string) => {
|
|||
});
|
||||
break;
|
||||
case 'saveAndConn':
|
||||
const res = await addHost(hostInfo);
|
||||
let res;
|
||||
if (form.id == 0) {
|
||||
res = await addHost(form);
|
||||
} else {
|
||||
res = await editHost(form);
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
if (form.isLocal) {
|
||||
emit('on-new-local');
|
||||
emit('load-host-tree');
|
||||
return;
|
||||
}
|
||||
let title = res.data.user + '@' + res.data.addr + ':' + res.data.port;
|
||||
if (res.data.name.length !== 0) {
|
||||
title = res.data.name + '-' + title;
|
||||
|
|
|
|||
|
|
@ -139,7 +139,12 @@
|
|||
></el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<HostDialog ref="dialogRef" @on-conn-terminal="onConnTerminal" @load-host-tree="loadHostTree" />
|
||||
<HostDialog
|
||||
ref="dialogRef"
|
||||
@on-conn-terminal="onConnTerminal"
|
||||
@on-new-local="onNewLocal"
|
||||
@load-host-tree="loadHostTree"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -152,7 +157,7 @@ import { ElTree } from 'element-plus';
|
|||
import screenfull from 'screenfull';
|
||||
import i18n from '@/lang';
|
||||
import { Host } from '@/api/interface/host';
|
||||
import { getHostTree, testByID } from '@/api/modules/terminal';
|
||||
import { getHostTree, testByID, testLocalConn } from '@/api/modules/terminal';
|
||||
import { GlobalStore } from '@/store';
|
||||
import router from '@/routers';
|
||||
import { getCommandTree } from '@/api/modules/command';
|
||||
|
|
@ -308,9 +313,14 @@ function beforeLeave(activeName: string) {
|
|||
}
|
||||
|
||||
const onNewSsh = () => {
|
||||
dialogRef.value!.acceptParams();
|
||||
dialogRef.value!.acceptParams({ isLocal: false });
|
||||
};
|
||||
const onNewLocal = () => {
|
||||
const onNewLocal = async () => {
|
||||
const res = await testLocalConn();
|
||||
if (!res.data) {
|
||||
dialogRef.value!.acceptParams({ isLocal: true });
|
||||
return;
|
||||
}
|
||||
terminalTabs.value.push({
|
||||
index: tabIndex,
|
||||
title: i18n.global.t('terminal.localhost'),
|
||||
|
|
@ -322,7 +332,7 @@ const onNewLocal = () => {
|
|||
nextTick(() => {
|
||||
ctx.refs[`t-${terminalValue.value}`] &&
|
||||
ctx.refs[`t-${terminalValue.value}`][0].acceptParams({
|
||||
endpoint: '/api/v2/hosts/exec',
|
||||
endpoint: '/api/v2/hosts/terminal',
|
||||
initCmd: initCmd.value,
|
||||
error: '',
|
||||
});
|
||||
|
|
@ -344,12 +354,13 @@ const onReconnect = async (item: any) => {
|
|||
}
|
||||
item.Refresh = !item.Refresh;
|
||||
if (item.wsID === 0) {
|
||||
const res = await testLocalConn();
|
||||
nextTick(() => {
|
||||
ctx.refs[`t-${item.index}`] &&
|
||||
ctx.refs[`t-${item.index}`][0].acceptParams({
|
||||
endpoint: '/api/v2/hosts/exec',
|
||||
endpoint: '/api/v2/hosts/terminal',
|
||||
initCmd: initCmd.value,
|
||||
error: '',
|
||||
error: res.data ? '' : 'Failed to set up the connection. Please check the host information',
|
||||
});
|
||||
initCmd.value = '';
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue