fix: Modify the terminal connection method (#8415)

This commit is contained in:
ssongliu 2025-04-17 22:26:23 +08:00 committed by GitHub
parent 779260145c
commit 5a1e010788
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 328 additions and 105 deletions

View file

@ -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)
}

View file

@ -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
}

View file

@ -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"`
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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
}

View file

@ -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) {

View file

@ -25,6 +25,7 @@ type SystemDir struct {
BaseDir string
DbDir string
LogDir string
TaskDir string
DataDir string
TmpDir string
LocalBackupDir string

View file

@ -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"))

View file

@ -28,6 +28,7 @@ func InitAgentDB() {
migrations.UpdateSettingStatus,
migrations.InitDefault,
migrations.UpdateWebsiteExpireDate,
migrations.AddLocalSSHSetting,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

View file

@ -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
},
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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()
}

View file

@ -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)

View file

@ -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;

View file

@ -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`);
};

View file

@ -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',

View file

@ -1050,6 +1050,7 @@ const message = {
terminal: {
local: 'ローカル',
localHelper: 'ローカル名はシステムのローカル識別にのみ使用されます',
connLocalErr: '自動的に認証できない場合はローカルサーバーのログイン情報を入力してください',
testConn: 'テスト接続',
saveAndConn: '保存して接続します',
connTestOk: '利用可能な接続情報',

View file

@ -1043,6 +1043,7 @@ const message = {
terminal: {
local: '로컬',
localHelper: '로컬 이름은 시스템 로컬 식별에만 사용됩니다.',
connLocalErr: '자동 인증에 실패했습니다. 로컬 서버 로그인 정보를 입력해주세요.',
testConn: '연결 테스트',
saveAndConn: '저장 연결',
connTestOk: '연결 정보가 유효합니다.',

View file

@ -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',

View file

@ -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',

View file

@ -1074,6 +1074,7 @@ const message = {
terminal: {
local: 'Локальный',
localHelper: 'Локальное имя используется только для локальной идентификации системы.',
connLocalErr: '無法自動認證請填寫本地服務器的登錄信息',
testConn: 'Проверить подключение',
saveAndConn: 'Сохранить и подключиться',
connTestOk: 'Информация о подключении доступна',

View file

@ -1040,6 +1040,7 @@ const message = {
terminal: {
local: '本機',
localHelper: 'local 名稱僅用於系統本機標識',
connLocalErr: '无法自动认证请填写本地服务器的登录信息',
testConn: '連接測試',
saveAndConn: '保存並連接',
connTestOk: '連接信息可用',

View file

@ -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;

View file

@ -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 = '';
});