feat: 数据库、redis、mongo支持ssh隧道等

This commit is contained in:
meilin.huang
2022-07-20 23:25:52 +08:00
parent 802e379f60
commit f0540559bb
30 changed files with 1885 additions and 1694 deletions

View File

@@ -1,6 +1,6 @@
Apache License Apache License
Version 2.0, January 2004 Version 2.0, January 2004
http://www.apache.org/licenses/ http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

View File

@@ -1,36 +0,0 @@
# mayfly-go
#### Description
golang实现linux运维等
#### Software Architecture
Software architecture description
#### Installation
1. xxxx
2. xxxx
3. xxxx
#### Instructions
1. xxxx
2. xxxx
3. xxxx
#### Contribution
1. Fork the repository
2. Create Feat_xxx branch
3. Commit your code
4. Create Pull Request
#### Gitee Feature
1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
2. Gitee blog [blog.gitee.com](https://blog.gitee.com)
3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
4. The most valuable open source project [GVP](https://gitee.com/gvp)
5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

View File

@@ -15,6 +15,7 @@
"cropperjs": "^1.5.11", "cropperjs": "^1.5.11",
"echarts": "^5.3.3", "echarts": "^5.3.3",
"element-plus": "^2.2.9", "element-plus": "^2.2.9",
"jsencrypt": "^3.2.1",
"jsoneditor": "^9.9.0", "jsoneditor": "^9.9.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mitt": "^3.0.0", "mitt": "^3.0.0",
@@ -24,7 +25,7 @@
"sql-formatter": "^7.0.3", "sql-formatter": "^7.0.3",
"vue": "^3.2.37", "vue": "^3.2.37",
"vue-clipboard3": "^1.0.1", "vue-clipboard3": "^1.0.1",
"vue-router": "^4.0.16", "vue-router": "^4.1.2",
"vuex": "^4.0.2", "vuex": "^4.0.2",
"xterm": "^4.19.0", "xterm": "^4.19.0",
"xterm-addon-fit": "^0.5.0" "xterm-addon-fit": "^0.5.0"
@@ -1880,6 +1881,11 @@
"js-yaml": "bin/js-yaml.js" "js-yaml": "bin/js-yaml.js"
} }
}, },
"node_modules/jsencrypt": {
"version": "3.2.1",
"resolved": "https://registry.npmmirror.com/jsencrypt/-/jsencrypt-3.2.1.tgz",
"integrity": "sha512-k1sD5QV0KPn+D8uG9AdGzTQuamt82QZ3A3l6f7TRwMU6Oi2Vg0BsL+wZIQBONcraO1pc78ExMdvmBBJ8WhNYUA=="
},
"node_modules/json-schema-traverse": { "node_modules/json-schema-traverse": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npm.taobao.org/json-schema-traverse/download/json-schema-traverse-0.4.1.tgz", "resolved": "https://registry.npm.taobao.org/json-schema-traverse/download/json-schema-traverse-0.4.1.tgz",
@@ -2931,15 +2937,11 @@
} }
}, },
"node_modules/vue-router": { "node_modules/vue-router": {
"version": "4.0.16", "version": "4.1.2",
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.0.16.tgz", "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.1.2.tgz",
"integrity": "sha512-JcO7cb8QJLBWE+DfxGUL3xUDOae/8nhM1KVdnudadTAORbuxIC/xAydC5Zr/VLHUDQi1ppuTF5/rjBGzgzrJNA==", "integrity": "sha512-5BP1qXFncVRwgV/XnqzsKApdMjQPqWIpoUBdL1ynz8HyLxIX/UDAx7Ql2BjmA5CXT/p61JfZvkpiFWFpaqcfag==",
"license": "MIT",
"dependencies": { "dependencies": {
"@vue/devtools-api": "^6.0.0" "@vue/devtools-api": "^6.1.4"
},
"funding": {
"url": "https://github.com/sponsors/posva"
}, },
"peerDependencies": { "peerDependencies": {
"vue": "^3.2.0" "vue": "^3.2.0"
@@ -4358,6 +4360,11 @@
"argparse": "^2.0.1" "argparse": "^2.0.1"
} }
}, },
"jsencrypt": {
"version": "3.2.1",
"resolved": "https://registry.npmmirror.com/jsencrypt/-/jsencrypt-3.2.1.tgz",
"integrity": "sha512-k1sD5QV0KPn+D8uG9AdGzTQuamt82QZ3A3l6f7TRwMU6Oi2Vg0BsL+wZIQBONcraO1pc78ExMdvmBBJ8WhNYUA=="
},
"json-schema-traverse": { "json-schema-traverse": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npm.taobao.org/json-schema-traverse/download/json-schema-traverse-0.4.1.tgz", "resolved": "https://registry.npm.taobao.org/json-schema-traverse/download/json-schema-traverse-0.4.1.tgz",
@@ -5044,11 +5051,11 @@
} }
}, },
"vue-router": { "vue-router": {
"version": "4.0.16", "version": "4.1.2",
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.0.16.tgz", "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.1.2.tgz",
"integrity": "sha512-JcO7cb8QJLBWE+DfxGUL3xUDOae/8nhM1KVdnudadTAORbuxIC/xAydC5Zr/VLHUDQi1ppuTF5/rjBGzgzrJNA==", "integrity": "sha512-5BP1qXFncVRwgV/XnqzsKApdMjQPqWIpoUBdL1ynz8HyLxIX/UDAx7Ql2BjmA5CXT/p61JfZvkpiFWFpaqcfag==",
"requires": { "requires": {
"@vue/devtools-api": "^6.0.0" "@vue/devtools-api": "^6.1.4"
} }
}, },
"vuex": { "vuex": {

View File

@@ -23,10 +23,13 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item prop="host" label="host:" required> <el-form-item prop="host" label="host:" required>
<el-input v-model.trim="form.host" placeholder="请输入主机ip" auto-complete="off"></el-input> <el-col :span="18">
</el-form-item> <el-input v-model.trim="form.host" placeholder="请输入主机ip" auto-complete="off"></el-input>
<el-form-item prop="port" label="port:" required> </el-col>
<el-input type="number" v-model.number="form.port" placeholder="请输入端口"></el-input> <el-col style="text-align: center" :span="1">:</el-col>
<el-col :span="5">
<el-input type="number" v-model.number="form.port" placeholder="请输入端口"></el-input>
</el-col>
</el-form-item> </el-form-item>
<el-form-item prop="username" label="用户名:" required> <el-form-item prop="username" label="用户名:" required>
<el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input> <el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
@@ -68,26 +71,22 @@
<el-button v-else class="ml5 mt5" size="small" @click="showInputDb"> + 添加数据库 </el-button> <el-button v-else class="ml5 mt5" size="small" @click="showInputDb"> + 添加数据库 </el-button>
</el-form-item> </el-form-item>
<el-form-item prop="enable_ssh" label="SSH:" v-if="form.type === 'mysql'"> <el-form-item prop="enableSshTunnel" label="SSH隧道:">
<el-checkbox v-model="form.enable_ssh" :true-label=1 :false-label=0></el-checkbox> <el-col :span="3">
</el-form-item> <el-checkbox @change="getSshTunnelMachines" v-model="form.enableSshTunnel" :true-label="1" :false-label="-1"></el-checkbox>
<el-form-item prop="ssh_host" label="SSH Host:" v-if="form.enable_ssh === 1 && form.type === 'mysql'"> </el-col>
<el-input v-model.trim="form.ssh_host" placeholder="请输入主机ip" auto-complete="off"></el-input> <el-col :span="2" v-if="form.enableSshTunnel == 1"> 机器: </el-col>
</el-form-item> <el-col :span="19" v-if="form.enableSshTunnel == 1">
<el-form-item prop="ssh_user" label="SSH User:" v-if="form.enable_ssh === 1 && form.type === 'mysql'"> <el-select style="width: 100%" v-model="form.sshTunnelMachineId" placeholder="请选择SSH隧道机器">
<el-input v-model.trim="form.ssh_user" placeholder="请输入用户名"></el-input> <el-option
</el-form-item> v-for="item in sshTunnelMachineList"
<el-form-item prop="ssh_pass" label="SSH Pass:" v-if="form.enable_ssh === 1 && form.type === 'mysql'"> :key="item.id"
<el-input :label="`${item.ip}:${item.port} [${item.name}]`"
type="password" :value="item.id"
show-password >
v-model.trim="form.ssh_pass" </el-option>
placeholder="请输入密码,修改操作可不填" </el-select>
autocomplete="new-password" </el-col>
></el-input>
</el-form-item>
<el-form-item prop="ssh_port" label="SSH Port:" v-if="form.enable_ssh === 1 && form.type === 'mysql'">
<el-input type="number" v-model.number="form.ssh_port" placeholder="请输入端口"></el-input>
</el-form-item> </el-form-item>
</el-form> </el-form>
@@ -105,6 +104,7 @@
import { toRefs, reactive, nextTick, watch, defineComponent, ref } from 'vue'; import { toRefs, reactive, nextTick, watch, defineComponent, ref } from 'vue';
import { dbApi } from './api'; import { dbApi } from './api';
import { projectApi } from '../project/api.ts'; import { projectApi } from '../project/api.ts';
import { machineApi } from '../machine/api.ts';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import type { ElInput } from 'element-plus'; import type { ElInput } from 'element-plus';
import { notBlank } from '@/common/assert'; import { notBlank } from '@/common/assert';
@@ -135,6 +135,7 @@ export default defineComponent({
projects: [], projects: [],
envs: [], envs: [],
databaseList: [] as any, databaseList: [] as any,
sshTunnelMachineList: [],
inputDbVisible: false, inputDbVisible: false,
inputDbValue: '', inputDbValue: '',
form: { form: {
@@ -149,11 +150,8 @@ export default defineComponent({
projectId: null, projectId: null,
envId: null, envId: null,
env: null, env: null,
enable_ssh: null, enableSshTunnel: null,
ssh_host: null, sshTunnelMachineId: null,
ssh_user: null,
ssh_pass: null,
ssh_port: 22,
}, },
btnLoading: false, btnLoading: false,
rules: { rules: {
@@ -188,14 +186,7 @@ export default defineComponent({
host: [ host: [
{ {
required: true, required: true,
message: '请输入主机ip', message: '请输入主机ip和port',
trigger: ['change', 'blur'],
},
],
port: [
{
required: true,
message: '请输入端口',
trigger: ['change', 'blur'], trigger: ['change', 'blur'],
}, },
], ],
@@ -217,6 +208,10 @@ export default defineComponent({
}); });
watch(props, (newValue) => { watch(props, (newValue) => {
state.dialogVisible = newValue.visible;
if (!state.dialogVisible) {
return;
}
state.projects = newValue.projects; state.projects = newValue.projects;
if (newValue.db) { if (newValue.db) {
getEnvs(newValue.db.projectId); getEnvs(newValue.db.projectId);
@@ -225,10 +220,10 @@ export default defineComponent({
state.databaseList = newValue.db.database.split(' '); state.databaseList = newValue.db.database.split(' ');
} else { } else {
state.envs = []; state.envs = [];
state.form = { port: 3306 } as any; state.form = { port: 3306, enableSshTunnel: -1 } as any;
state.databaseList = []; state.databaseList = [];
} }
state.dialogVisible = newValue.visible; getSshTunnelMachines();
}); });
const handleClose = (db: string) => { const handleClose = (db: string) => {
@@ -259,6 +254,13 @@ export default defineComponent({
state.form.database = state.databaseList.length == 0 ? '' : state.databaseList.join(' '); state.form.database = state.databaseList.length == 0 ? '' : state.databaseList.join(' ');
}; };
const getSshTunnelMachines = async () => {
if (state.form.enableSshTunnel == 1 && state.sshTunnelMachineList.length == 0) {
const res = await machineApi.list.request({ pageNum: 1, pageSize: 100 });
state.sshTunnelMachineList = res.list;
}
};
const getEnvs = async (projectId: any) => { const getEnvs = async (projectId: any) => {
state.envs = await projectApi.projectEnvs.request({ projectId }); state.envs = await projectApi.projectEnvs.request({ projectId });
}; };
@@ -291,7 +293,7 @@ export default defineComponent({
if (valid) { if (valid) {
const reqForm = { ...state.form }; const reqForm = { ...state.form };
reqForm.password = await RsaEncrypt(reqForm.password); reqForm.password = await RsaEncrypt(reqForm.password);
reqForm.ssh_pass = await RsaEncrypt(reqForm.ssh_pass); // reqForm.ssh_pass = await RsaEncrypt(reqForm.ssh_pass);
dbApi.saveDb.request(reqForm).then(() => { dbApi.saveDb.request(reqForm).then(() => {
ElMessage.success('保存成功'); ElMessage.success('保存成功');
emit('val-change', state.form); emit('val-change', state.form);
@@ -330,6 +332,7 @@ export default defineComponent({
handleClose, handleClose,
showInputDb, showInputDb,
handleInputDbConfirm, handleInputDbConfirm,
getSshTunnelMachines,
changeProject, changeProject,
changeEnv, changeEnv,
btnOk, btnOk,

View File

@@ -11,15 +11,24 @@
<el-input v-model.trim="form.name" placeholder="请输入机器别名" auto-complete="off"></el-input> <el-input v-model.trim="form.name" placeholder="请输入机器别名" auto-complete="off"></el-input>
</el-form-item> </el-form-item>
<el-form-item prop="ip" label="ip:" required> <el-form-item prop="ip" label="ip:" required>
<el-input v-model.trim="form.ip" placeholder="请输入主机ip" auto-complete="off"></el-input> <el-col :span="18">
</el-form-item> <el-input v-model.trim="form.ip" placeholder="主机ip" auto-complete="off"></el-input>
<el-form-item prop="port" label="port:" required> </el-col>
<el-input type="number" v-model.number="form.port" placeholder="请输入端口"></el-input> <el-col style="text-align: center" :span="1">:</el-col>
<el-col :span="5">
<el-input type="number" v-model.number="form.port" placeholder="端口"></el-input>
</el-col>
</el-form-item> </el-form-item>
<el-form-item prop="username" label="用户名:" required> <el-form-item prop="username" label="用户名:" required>
<el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input> <el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
</el-form-item> </el-form-item>
<el-form-item prop="password" label="密码:"> <el-form-item prop="authMethod" label="认证方式:" required>
<el-select style="width: 100%" v-model="form.authMethod" placeholder="请选择认证方式">
<el-option key="1" label="Password" :value="1"> </el-option>
<el-option key="2" label="PublicKey" :value="2"> </el-option>
</el-select>
</el-form-item>
<el-form-item v-if="form.authMethod == 1" prop="password" label="密码:">
<el-input <el-input
type="password" type="password"
show-password show-password
@@ -28,6 +37,9 @@
autocomplete="new-password" autocomplete="new-password"
></el-input> ></el-input>
</el-form-item> </el-form-item>
<el-form-item v-if="form.authMethod == 2" prop="password" label="秘钥:">
<el-input type="textarea" :rows="3" v-model="form.password" placeholder="请将私钥文件内容拷贝至此,修改操作可不填"></el-input>
</el-form-item>
<el-form-item prop="remark" label="备注:"> <el-form-item prop="remark" label="备注:">
<el-input type="textarea" v-model="form.remark"></el-input> <el-input type="textarea" v-model="form.remark"></el-input>
</el-form-item> </el-form-item>
@@ -76,9 +88,10 @@ export default defineComponent({
projectId: null, projectId: null,
projectName: null, projectName: null,
name: null, name: null,
authMethod: 1,
port: 22, port: 22,
username: "", username: '',
password: "", password: '',
remark: '', remark: '',
}, },
btnLoading: false, btnLoading: false,
@@ -107,14 +120,7 @@ export default defineComponent({
ip: [ ip: [
{ {
required: true, required: true,
message: '请输入主机ip', message: '请输入主机ip和端口',
trigger: ['change', 'blur'],
},
],
port: [
{
required: true,
message: '请输入端口',
trigger: ['change', 'blur'], trigger: ['change', 'blur'],
}, },
], ],
@@ -125,16 +131,26 @@ export default defineComponent({
trigger: ['change', 'blur'], trigger: ['change', 'blur'],
}, },
], ],
authMethod: [
{
required: true,
message: '请选择认证方式',
trigger: ['change', 'blur'],
},
],
}, },
}); });
watch(props, async (newValue) => { watch(props, async (newValue) => {
state.dialogVisible = newValue.visible; state.dialogVisible = newValue.visible;
if (!state.dialogVisible) {
return;
}
state.projects = newValue.projects; state.projects = newValue.projects;
if (newValue.machine) { if (newValue.machine) {
state.form = { ...newValue.machine }; state.form = { ...newValue.machine };
} else { } else {
state.form = { port: 22 } as any; state.form = { port: 22, authMethod: 1 } as any;
} }
}); });
@@ -153,17 +169,18 @@ export default defineComponent({
machineForm.value.validate(async (valid: boolean) => { machineForm.value.validate(async (valid: boolean) => {
if (valid) { if (valid) {
const reqForm = { ...state.form }; const reqForm = { ...state.form };
reqForm.password = await RsaEncrypt(state.form.password); if (reqForm.authMethod == 1) {
machineApi.saveMachine.request(reqForm).then(() => { reqForm.password = await RsaEncrypt(state.form.password);
}
state.btnLoading = true;
try {
await machineApi.saveMachine.request(reqForm);
ElMessage.success('保存成功'); ElMessage.success('保存成功');
emit('val-change', state.form); emit('val-change', state.form);
state.btnLoading = true;
setTimeout(() => {
state.btnLoading = false;
}, 1000);
cancel(); cancel();
}); } finally {
state.btnLoading = false;
}
} else { } else {
ElMessage.error('请正确填写信息'); ElMessage.error('请正确填写信息');
return false; return false;

View File

@@ -260,13 +260,13 @@ export default defineComponent({
search(); search();
}; };
const openFormDialog = (redis: any) => { const openFormDialog = (machine: any) => {
let dialogTitle; let dialogTitle;
if (redis) { if (machine) {
state.machineEditDialog.data = state.currentData as any; state.machineEditDialog.data = state.currentData as any;
dialogTitle = '编辑机器'; dialogTitle = '编辑机器';
} else { } else {
state.machineEditDialog.data = { port: 22 } as any; state.machineEditDialog.data = null;
dialogTitle = '添加机器'; dialogTitle = '添加机器';
} }

View File

@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :close-on-click-modal="false" width="35%" :destroy-on-close="true"> <el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :close-on-click-modal="false" width="38%" :destroy-on-close="true">
<el-form :model="form" ref="mongoForm" :rules="rules" label-width="65px"> <el-form :model="form" ref="mongoForm" :rules="rules" label-width="85px">
<el-form-item prop="projectId" label="项目" required> <el-form-item prop="projectId" label="项目" required>
<el-select style="width: 100%" v-model="form.projectId" placeholder="请选择项目" @change="changeProject" filterable> <el-select style="width: 100%" v-model="form.projectId" placeholder="请选择项目" @change="changeProject" filterable>
<el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option> <el-option v-for="item in projects" :key="item.id" :label="`${item.name} [${item.remark}]`" :value="item.id"> </el-option>
@@ -25,6 +25,24 @@
auto-complete="off" auto-complete="off"
></el-input> ></el-input>
</el-form-item> </el-form-item>
<el-form-item prop="enableSshTunnel" label="SSH隧道:">
<el-col :span="3">
<el-checkbox @change="getSshTunnelMachines" v-model="form.enableSshTunnel" :true-label="1" :false-label="-1"></el-checkbox>
</el-col>
<el-col :span="2" v-if="form.enableSshTunnel == 1"> 机器: </el-col>
<el-col :span="19" v-if="form.enableSshTunnel == 1">
<el-select style="width: 100%" v-model="form.sshTunnelMachineId" placeholder="请选择SSH隧道机器">
<el-option
v-for="item in sshTunnelMachineList"
:key="item.id"
:label="`${item.ip}:${item.port} [${item.name}]`"
:value="item.id"
>
</el-option>
</el-select>
</el-col>
</el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
@@ -41,6 +59,7 @@
import { toRefs, reactive, watch, defineComponent, ref } from 'vue'; import { toRefs, reactive, watch, defineComponent, ref } from 'vue';
import { mongoApi } from './api'; import { mongoApi } from './api';
import { projectApi } from '../project/api.ts'; import { projectApi } from '../project/api.ts';
import { machineApi } from '../machine/api.ts';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { RsaEncrypt } from '@/common/rsa'; import { RsaEncrypt } from '@/common/rsa';
@@ -66,10 +85,13 @@ export default defineComponent({
dialogVisible: false, dialogVisible: false,
projects: [], projects: [],
envs: [], envs: [],
sshTunnelMachineList: [],
form: { form: {
id: null, id: null,
name: null, name: null,
uri: null, uri: null,
enableSshTunnel: -1,
sshTunnelMachineId: null,
project: null, project: null,
projectId: null, projectId: null,
envId: null, envId: null,
@@ -110,6 +132,9 @@ export default defineComponent({
watch(props, async (newValue) => { watch(props, async (newValue) => {
state.dialogVisible = newValue.visible; state.dialogVisible = newValue.visible;
if (!state.dialogVisible) {
return;
}
state.projects = newValue.projects; state.projects = newValue.projects;
if (newValue.mongo) { if (newValue.mongo) {
getEnvs(newValue.mongo.projectId); getEnvs(newValue.mongo.projectId);
@@ -118,8 +143,16 @@ export default defineComponent({
state.envs = []; state.envs = [];
state.form = { db: 0 } as any; state.form = { db: 0 } as any;
} }
getSshTunnelMachines();
}); });
const getSshTunnelMachines = async () => {
if (state.form.enableSshTunnel == 1 && state.sshTunnelMachineList.length == 0) {
const res = await machineApi.list.request({ pageNum: 1, pageSize: 100 });
state.sshTunnelMachineList = res.list;
}
};
const getEnvs = async (projectId: any) => { const getEnvs = async (projectId: any) => {
state.envs = await projectApi.projectEnvs.request({ projectId }); state.envs = await projectApi.projectEnvs.request({ projectId });
}; };
@@ -175,6 +208,7 @@ export default defineComponent({
...toRefs(state), ...toRefs(state),
mongoForm, mongoForm,
changeProject, changeProject,
getSshTunnelMachines,
changeEnv, changeEnv,
btnOk, btnOk,
cancel, cancel,

View File

@@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :close-on-click-modal="false" :destroy-on-close="true" width="35%"> <el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :close-on-click-modal="false" :destroy-on-close="true" width="38%">
<el-form :model="form" ref="redisForm" :rules="rules" label-width="85px"> <el-form :model="form" ref="redisForm" :rules="rules" label-width="85px">
<el-form-item prop="projectId" label="项目:" required> <el-form-item prop="projectId" label="项目:" required>
<el-select style="width: 100%" v-model="form.projectId" placeholder="请选择项目" @change="changeProject" filterable> <el-select style="width: 100%" v-model="form.projectId" placeholder="请选择项目" @change="changeProject" filterable>
@@ -42,6 +42,23 @@
<el-form-item prop="remark" label="备注:"> <el-form-item prop="remark" label="备注:">
<el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input> <el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input>
</el-form-item> </el-form-item>
<el-form-item prop="enableSshTunnel" label="SSH隧道:">
<el-col :span="3">
<el-checkbox @change="getSshTunnelMachines" v-model="form.enableSshTunnel" :true-label="1" :false-label="-1"></el-checkbox>
</el-col>
<el-col :span="2" v-if="form.enableSshTunnel == 1"> 机器: </el-col>
<el-col :span="19" v-if="form.enableSshTunnel == 1">
<el-select style="width: 100%" v-model="form.sshTunnelMachineId" placeholder="请选择SSH隧道机器">
<el-option
v-for="item in sshTunnelMachineList"
:key="item.id"
:label="`${item.ip}:${item.port} [${item.name}]`"
:value="item.id"
>
</el-option>
</el-select>
</el-col>
</el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
@@ -58,6 +75,7 @@
import { toRefs, reactive, watch, defineComponent, ref } from 'vue'; import { toRefs, reactive, watch, defineComponent, ref } from 'vue';
import { redisApi } from './api'; import { redisApi } from './api';
import { projectApi } from '../project/api.ts'; import { projectApi } from '../project/api.ts';
import { machineApi } from '../machine/api.ts';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { RsaEncrypt } from '@/common/rsa'; import { RsaEncrypt } from '@/common/rsa';
@@ -83,6 +101,7 @@ export default defineComponent({
dialogVisible: false, dialogVisible: false,
projects: [], projects: [],
envs: [], envs: [],
sshTunnelMachineList: [],
form: { form: {
id: null, id: null,
name: null, name: null,
@@ -94,6 +113,8 @@ export default defineComponent({
envId: null, envId: null,
env: null, env: null,
remark: '', remark: '',
enableSshTunnel: null,
sshTunnelMachineId: null,
}, },
btnLoading: false, btnLoading: false,
rules: { rules: {
@@ -137,16 +158,27 @@ export default defineComponent({
watch(props, async (newValue) => { watch(props, async (newValue) => {
state.dialogVisible = newValue.visible; state.dialogVisible = newValue.visible;
if (!state.dialogVisible) {
return;
}
state.projects = newValue.projects; state.projects = newValue.projects;
if (newValue.redis) { if (newValue.redis) {
getEnvs(newValue.redis.projectId); getEnvs(newValue.redis.projectId);
state.form = { ...newValue.redis }; state.form = { ...newValue.redis };
} else { } else {
state.envs = []; state.envs = [];
state.form = { db: 0 } as any; state.form = { db: 0, enableSshTunnel: -1 } as any;
} }
getSshTunnelMachines();
}); });
const getSshTunnelMachines = async () => {
if (state.form.enableSshTunnel == 1 && state.sshTunnelMachineList.length == 0) {
const res = await machineApi.list.request({ pageNum: 1, pageSize: 100 });
state.sshTunnelMachineList = res.list;
}
};
const getEnvs = async (projectId: any) => { const getEnvs = async (projectId: any) => {
state.envs = await projectApi.projectEnvs.request({ projectId }); state.envs = await projectApi.projectEnvs.request({ projectId });
}; };
@@ -201,6 +233,7 @@ export default defineComponent({
return { return {
...toRefs(state), ...toRefs(state),
redisForm, redisForm,
getSshTunnelMachines,
changeProject, changeProject,
changeEnv, changeEnv,
btnOk, btnOk,

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
app: app:
name: mayfly-go name: mayfly-go
version: 1.2.0 version: 1.2.3
server: server:
# debug release test # debug release test

View File

@@ -6,6 +6,7 @@ require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible // jwt github.com/dgrijalva/jwt-go v3.2.0+incompatible // jwt
github.com/gin-gonic/gin v1.8.1 github.com/gin-gonic/gin v1.8.1
github.com/go-redis/redis/v8 v8.11.5 github.com/go-redis/redis/v8 v8.11.5
github.com/go-sql-driver/mysql v1.6.0
github.com/gorilla/websocket v1.5.0 github.com/gorilla/websocket v1.5.0
github.com/lib/pq v1.10.6 github.com/lib/pq v1.10.6
github.com/mojocn/base64Captcha v1.3.5 // github.com/mojocn/base64Captcha v1.3.5 //
@@ -28,7 +29,6 @@ require (
github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.10.1 // indirect github.com/go-playground/validator/v10 v10.10.1 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/go-stack/stack v1.8.0 // indirect github.com/go-stack/stack v1.8.0 // indirect
github.com/goccy/go-json v0.9.7 // indirect github.com/goccy/go-json v0.9.7 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect

View File

@@ -56,13 +56,13 @@ func (d *Db) Save(rc *ctx.ReqCtx) {
// 密码脱敏记录日志 // 密码脱敏记录日志
form.Password = "****" form.Password = "****"
if form.Type == "mysql" && form.EnableSSH == 1 { // if form.Type == "mysql" && form.EnableSSH == 1 {
originSSHPwd, err := utils.DefaultRsaDecrypt(form.SSHPass, true) // // originSSHPwd, err := utils.DefaultRsaDecrypt(form.SSHPass, true)
biz.ErrIsNilAppendErr(err, "解密密码错误: %s") // biz.ErrIsNilAppendErr(err, "解密密码错误: %s")
db.SSHPass = originSSHPwd // // db.SSHPass = originSSHPwd
// 密码脱敏记录日志 // // 密码脱敏记录日志
form.SSHPass = "****" // form.SSHPass = "****"
} // }
rc.ReqParam = form rc.ReqParam = form

View File

@@ -15,11 +15,8 @@ type DbForm struct {
Env string `json:"env"` Env string `json:"env"`
EnvId uint64 `binding:"required" json:"envId"` EnvId uint64 `binding:"required" json:"envId"`
EnableSSH int `json:"enable_ssh"` EnableSshTunnel int8 `json:"enableSshTunnel"`
SSHHost string `json:"ssh_host"` SshTunnelMachineId uint64 `json:"sshTunnelMachineId"`
SSHPort int `json:"ssh_port"`
SSHUser string `json:"ssh_user"`
SSHPass string `json:"ssh_pass"`
} }
type DbSqlSaveForm struct { type DbSqlSaveForm struct {

View File

@@ -5,14 +5,12 @@ type MachineForm struct {
ProjectId uint64 `json:"projectId"` ProjectId uint64 `json:"projectId"`
ProjectName string `json:"projectName"` ProjectName string `json:"projectName"`
Name string `json:"name" binding:"required"` Name string `json:"name" binding:"required"`
// IP地址 Ip string `json:"ip" binding:"required"` // IP地址
Ip string `json:"ip" binding:"required"` Username string `json:"username" binding:"required"` // 用户名
// 用户名 AuthMethod int8 `json:"authMethod" binding:"required"`
Username string `json:"username" binding:"required"` Password string `json:"password"`
Password string `json:"password"` Port int `json:"port" binding:"required"` // 端口号
// 端口号 Remark string `json:"remark"`
Port int `json:"port" binding:"required"`
Remark string `json:"remark"`
} }
type MachineRunForm struct { type MachineRunForm struct {

View File

@@ -1,13 +1,15 @@
package form package form
type Mongo struct { type Mongo struct {
Id uint64 Id uint64
Uri string `binding:"required" json:"uri"` Uri string `binding:"required" json:"uri"`
Name string `binding:"required" json:"name"` EnableSshTunnel int8 `json:"enableSshTunnel"` // 是否启用ssh隧道
ProjectId uint64 `binding:"required" json:"projectId"` SshTunnelMachineId uint64 `json:"sshTunnelMachineId"` // ssh隧道机器id
Project string `json:"project"` Name string `binding:"required" json:"name"`
Env string `json:"env"` ProjectId uint64 `binding:"required" json:"projectId"`
EnvId uint64 `binding:"required" json:"envId"` Project string `json:"project"`
Env string `json:"env"`
EnvId uint64 `binding:"required" json:"envId"`
} }
type MongoCommand struct { type MongoCommand struct {

View File

@@ -1,16 +1,18 @@
package form package form
type Redis struct { type Redis struct {
Id uint64 Id uint64
Host string `binding:"required" json:"host"` Host string `binding:"required" json:"host"`
Password string `json:"password"` Password string `json:"password"`
Mode string `json:"mode"` Mode string `json:"mode"`
Db int `json:"db"` Db int `json:"db"`
ProjectId uint64 `binding:"required" json:"projectId"` EnableSshTunnel int8 `json:"enableSshTunnel"` // 是否启用ssh隧道
Project string `json:"project"` SshTunnelMachineId uint64 `json:"sshTunnelMachineId"` // ssh隧道机器id
Env string `json:"env"` ProjectId uint64 `binding:"required" json:"projectId"`
EnvId uint64 `binding:"required" json:"envId"` Project string `json:"project"`
Remark string `json:"remark"` Env string `json:"env"`
EnvId uint64 `binding:"required" json:"envId"`
Remark string `json:"remark"`
} }
type KeyInfo struct { type KeyInfo struct {

View File

@@ -54,20 +54,22 @@ func (m *Machine) SaveMachine(rc *ctx.ReqCtx) {
machineForm := new(form.MachineForm) machineForm := new(form.MachineForm)
ginx.BindJsonAndValid(g, machineForm) ginx.BindJsonAndValid(g, machineForm)
entity := new(entity.Machine) me := new(entity.Machine)
utils.Copy(entity, machineForm) utils.Copy(me, machineForm)
// 密码解密,并使用解密后的赋值 if me.AuthMethod == entity.MachineAuthMethodPassword {
originPwd, err := utils.DefaultRsaDecrypt(machineForm.Password, true) // 密码解密,并使用解密后的赋值
biz.ErrIsNilAppendErr(err, "解密密码错误: %s") originPwd, err := utils.DefaultRsaDecrypt(machineForm.Password, true)
entity.Password = originPwd biz.ErrIsNilAppendErr(err, "解密密码错误: %s")
me.Password = originPwd
}
// 密码脱敏记录日志 // 密码脱敏记录日志
machineForm.Password = "****" machineForm.Password = "****"
rc.ReqParam = machineForm rc.ReqParam = machineForm
entity.SetBaseInfo(rc.LoginAccount) me.SetBaseInfo(rc.LoginAccount)
m.MachineApp.Save(entity) m.MachineApp.Save(me)
} }
func (m *Machine) ChangeStatus(rc *ctx.ReqCtx) { func (m *Machine) ChangeStatus(rc *ctx.ReqCtx) {

View File

@@ -20,8 +20,6 @@ type SelectDataDbVO struct {
Creator *string `json:"creator"` Creator *string `json:"creator"`
CreatorId *int64 `json:"creatorId"` CreatorId *int64 `json:"creatorId"`
EnableSSH *int `json:"enable_ssh"` EnableSshTunnel *int8 `json:"enableSshTunnel"`
SSHHost *string `json:"ssh_host"` SshTunnelMachineId *uint64 `json:"sshTunnelMachineId"`
SSHPort *int `json:"ssh_port"`
SSHUser *string `json:"ssh_user"`
} }

View File

@@ -5,17 +5,19 @@ import "time"
type Redis struct { type Redis struct {
Id *int64 `json:"id"` Id *int64 `json:"id"`
// Name *string `json:"name"` // Name *string `json:"name"`
Host *string `json:"host"` Host *string `json:"host"`
Db int `json:"db"` Db int `json:"db"`
ProjectId *int64 `json:"projectId"` ProjectId *int64 `json:"projectId"`
Project *string `json:"project"` Project *string `json:"project"`
Mode *string `json:"mode"` Mode *string `json:"mode"`
Remark *string `json:"remark"` EnableSshTunnel *int8 `orm:"column(enable_ssh_tunnel)" json:"enableSshTunnel"` // 是否启用ssh隧道
Env *string `json:"env"` SshTunnelMachineId *uint64 `orm:"column(ssh_tunnel_machine_id)" json:"sshTunnelMachineId"` // ssh隧道机器id
EnvId *int64 `json:"envId"` Remark *string `json:"remark"`
CreateTime *time.Time `json:"createTime"` Env *string `json:"env"`
Creator *string `json:"creator"` EnvId *int64 `json:"envId"`
CreatorId *int64 `json:"creatorId"` CreateTime *time.Time `json:"createTime"`
Creator *string `json:"creator"`
CreatorId *int64 `json:"creatorId"`
} }
type Keys struct { type Keys struct {

View File

@@ -22,6 +22,7 @@ type MachineVO struct {
Username *string `json:"username"` Username *string `json:"username"`
Ip *string `json:"ip"` Ip *string `json:"ip"`
Port *int `json:"port"` Port *int `json:"port"`
AuthMethod *int8 `json:"authMethod"`
Status *int8 `json:"status"` Status *int8 `json:"status"`
CreateTime *time.Time `json:"createTime"` CreateTime *time.Time `json:"createTime"`
Creator *string `json:"creator"` Creator *string `json:"creator"`

View File

@@ -1,11 +1,13 @@
package application package application
import ( import (
"context"
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"mayfly-go/internal/devops/domain/entity" "mayfly-go/internal/devops/domain/entity"
"mayfly-go/internal/devops/domain/repository" "mayfly-go/internal/devops/domain/repository"
"mayfly-go/internal/devops/infrastructure/machine"
"mayfly-go/internal/devops/infrastructure/persistence" "mayfly-go/internal/devops/infrastructure/persistence"
"mayfly-go/pkg/biz" "mayfly-go/pkg/biz"
"mayfly-go/pkg/cache" "mayfly-go/pkg/cache"
@@ -20,7 +22,8 @@ import (
"time" "time"
"github.com/go-sql-driver/mysql" "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq" "github.com/lib/pq"
"golang.org/x/crypto/ssh"
) )
type Db interface { type Db interface {
@@ -77,15 +80,11 @@ func (d *dbAppImpl) GetById(id uint64, cols ...string) *entity.Db {
func (d *dbAppImpl) Save(dbEntity *entity.Db) { func (d *dbAppImpl) Save(dbEntity *entity.Db) {
// 默认tcp连接 // 默认tcp连接
if dbEntity.Type == "mysql" && dbEntity.EnableSSH == 1 { dbEntity.Network = dbEntity.GetNetwork()
dbEntity.Network = "mysql+ssh"
} else {
dbEntity.Network = "tcp"
}
// 测试连接 // 测试连接
if dbEntity.Password != "" { if dbEntity.Password != "" {
TestConnection(*dbEntity) TestConnection(dbEntity)
} }
// 查找是否存在该库 // 查找是否存在该库
@@ -109,6 +108,8 @@ func (d *dbAppImpl) Save(dbEntity *entity.Db) {
var oldDbs []interface{} var oldDbs []interface{}
for _, v := range strings.Split(old.Database, " ") { for _, v := range strings.Split(old.Database, " ") {
// 关闭数据库连接
CloseDb(dbEntity.Id, v)
oldDbs = append(oldDbs, v) oldDbs = append(oldDbs, v)
} }
@@ -121,14 +122,11 @@ func (d *dbAppImpl) Save(dbEntity *entity.Db) {
return i1.(string) == i2.(string) return i1.(string) == i2.(string)
}) })
for _, v := range delDb { for _, v := range delDb {
// 先关闭数据库连接
CloseDb(dbEntity.Id, v.(string))
// 删除该库关联的所有sql记录 // 删除该库关联的所有sql记录
d.dbSqlRepo.DeleteBy(&entity.DbSql{DbId: dbId, Db: v.(string)}) d.dbSqlRepo.DeleteBy(&entity.DbSql{DbId: dbId, Db: v.(string)})
} }
d.dbRepo.Update(dbEntity) d.dbRepo.Update(dbEntity)
} }
func (d *dbAppImpl) Delete(id uint64) { func (d *dbAppImpl) Delete(id uint64) {
@@ -160,28 +158,43 @@ func (da *dbAppImpl) GetDbInstance(id uint64, db string) *DbInstance {
d := da.GetById(id) d := da.GetById(id)
biz.NotNil(d, "数据库信息不存在") biz.NotNil(d, "数据库信息不存在")
biz.IsTrue(strings.Contains(d.Database, db), "未配置该库的操作权限") biz.IsTrue(strings.Contains(d.Database, db), "未配置该库的操作权限")
global.Log.Infof("连接db: %s:%d/%s", d.Host, d.Port, db)
cacheKey := GetDbCacheKey(id, db)
dbi := &DbInstance{Id: cacheKey, Type: d.Type, ProjectId: d.ProjectId}
//SSH Conect //SSH Conect
if d.Type == "mysql" && d.EnableSSH == 1 { if d.EnableSshTunnel == 1 && d.SshTunnelMachineId != 0 {
sshClient, err := utils.SSHConnect(d.SSHUser, d.SSHPass, d.SSHHost, "", d.SSHPort) me := MachineApp.GetById(d.SshTunnelMachineId)
if err != nil { biz.NotNil(me, "隧道机器信息不存在")
global.Log.Errorf("ssh连接失败: %s@%s:%d", d.SSHUser, d.SSHHost, d.SSHPort) sshClient, err := machine.GetSshClient(me)
panic(biz.NewBizErr(fmt.Sprintf("ssh连接失败: %s", err.Error()))) biz.ErrIsNilAppendErr(err, "ssh隧道连接失败: %s")
dbi.sshTunnel = sshClient
if d.Type == entity.DbTypeMysql {
mysql.RegisterDialContext(d.Network, func(ctx context.Context, addr string) (net.Conn, error) {
return sshClient.Dial("tcp", addr)
})
} else if d.Type == entity.DbTypePostgres {
_, err := pq.DialOpen(&PqSqlDialer{sshTunnel: sshClient}, getDsn(d))
if err != nil {
dbi.Close()
panic(biz.NewBizErr(fmt.Sprintf("postgres隧道连接失败: %s", err.Error())))
}
} }
mysql.RegisterDial("mysql+ssh", func(addr string) (net.Conn, error) {
return sshClient.Dial("tcp", addr)
})
} }
// 将数据库替换为要访问的数据库,原本数据库为空格拼接的所有库 // 将数据库替换为要访问的数据库,原本数据库为空格拼接的所有库
d.Database = db d.Database = db
DB, err := sql.Open(d.Type, getDsn(d)) DB, err := sql.Open(d.Type, getDsn(d))
biz.ErrIsNil(err, fmt.Sprintf("Open %s failed, err:%v\n", d.Type, err)) if err != nil {
perr := DB.Ping() dbi.Close()
if perr != nil { panic(biz.NewBizErr(fmt.Sprintf("Open %s failed, err:%v\n", d.Type, err)))
}
err = DB.Ping()
if err != nil {
dbi.Close()
global.Log.Errorf("连接db失败: %s:%d/%s", d.Host, d.Port, db) global.Log.Errorf("连接db失败: %s:%d/%s", d.Host, d.Port, db)
panic(biz.NewBizErr(fmt.Sprintf("数据库连接失败: %s", perr.Error()))) panic(biz.NewBizErr(fmt.Sprintf("数据库连接失败: %s", err.Error())))
} }
// 最大连接周期超过时间的连接就close // 最大连接周期超过时间的连接就close
@@ -191,14 +204,30 @@ func (da *dbAppImpl) GetDbInstance(id uint64, db string) *DbInstance {
// 设置闲置连接数 // 设置闲置连接数
DB.SetMaxIdleConns(1) DB.SetMaxIdleConns(1)
cacheKey := GetDbCacheKey(id, db) dbi.db = DB
dbi := &DbInstance{Id: cacheKey, Type: d.Type, ProjectId: d.ProjectId, db: DB} global.Log.Infof("连接db: %s:%d/%s", d.Host, d.Port, db)
if needCache { if needCache {
dbCache.Put(cacheKey, dbi) dbCache.Put(cacheKey, dbi)
} }
return dbi return dbi
} }
type PqSqlDialer struct {
sshTunnel *ssh.Client
}
func (pd *PqSqlDialer) Dial(network, address string) (net.Conn, error) {
if sshConn, err := pd.sshTunnel.Dial(network, address); err == nil {
// 将ssh conn包装否则redis内部设置超时会报错,ssh conn不支持设置超时会返回错误: ssh: tcpChan: deadline not supported
return &utils.WrapSshConn{Conn: sshConn}, nil
} else {
return nil, err
}
}
func (pd *PqSqlDialer) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
return pd.Dial(network, address)
}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// 客户端连接缓存30分钟内没有访问则会被关闭, key为数据库实例id:数据库 // 客户端连接缓存30分钟内没有访问则会被关闭, key为数据库实例id:数据库
@@ -220,22 +249,28 @@ func GetDbInstanceByCache(id string) *DbInstance {
return nil return nil
} }
func TestConnection(d entity.Db) { func TestConnection(d *entity.Db) {
//SSH Conect //SSH Conect
if d.Type == "mysql" && d.EnableSSH == 1 { if d.EnableSshTunnel == 1 && d.SshTunnelMachineId != 0 {
sshClient, err := utils.SSHConnect(d.SSHUser, d.SSHPass, d.SSHHost, "", d.SSHPort) me := MachineApp.GetById(d.SshTunnelMachineId)
if err != nil { sshClient, err := machine.GetSshClient(me)
global.Log.Errorf("ssh连接失败: %s@%s:%d", d.SSHUser, d.SSHHost, d.SSHPort) biz.ErrIsNilAppendErr(err, "ssh隧道连接失败: %s")
panic(biz.NewBizErr(fmt.Sprintf("ssh连接失败: %s", err.Error()))) defer sshClient.Close()
if d.Type == entity.DbTypeMysql {
mysql.RegisterDialContext(d.Network, func(ctx context.Context, addr string) (net.Conn, error) {
return sshClient.Dial("tcp", addr)
})
} else if d.Type == entity.DbTypePostgres {
_, err := pq.DialOpen(&PqSqlDialer{sshTunnel: sshClient}, getDsn(d))
if err != nil {
panic(biz.NewBizErr(fmt.Sprintf("postgres隧道连接失败: %s", err.Error())))
}
} }
mysql.RegisterDial("mysql+ssh", func(addr string) (net.Conn, error) {
return sshClient.Dial("tcp", addr)
})
} }
// 验证第一个库是否可以连接即可 // 验证第一个库是否可以连接即可
d.Database = strings.Split(d.Database, " ")[0] d.Database = strings.Split(d.Database, " ")[0]
DB, err := sql.Open(d.Type, getDsn(&d)) DB, err := sql.Open(d.Type, getDsn(d))
biz.ErrIsNil(err, "Open %s failed, err:%v\n", d.Type, err) biz.ErrIsNil(err, "Open %s failed, err:%v\n", d.Type, err)
defer DB.Close() defer DB.Close()
perr := DB.Ping() perr := DB.Ping()
@@ -248,6 +283,7 @@ type DbInstance struct {
Type string Type string
ProjectId uint64 ProjectId uint64
db *sql.DB db *sql.DB
sshTunnel *ssh.Client
} }
// 执行查询语句 // 执行查询语句
@@ -359,13 +395,22 @@ func (d *DbInstance) Exec(sql string) (int64, error) {
// 关闭连接 // 关闭连接
func (d *DbInstance) Close() { func (d *DbInstance) Close() {
d.db.Close() if d.db != nil {
if err := d.db.Close(); err != nil {
global.Log.Errorf("关闭数据库实例[%s]连接失败: %s", d.Id, err.Error())
}
}
if d.sshTunnel != nil {
if err := d.sshTunnel.Close(); err != nil {
global.Log.Errorf("关闭数据库实例[%s]的ssh隧道失败: %s", d.Id, err.Error())
}
}
} }
// 获取dataSourceName // 获取dataSourceName
func getDsn(d *entity.Db) string { func getDsn(d *entity.Db) string {
var dsn string var dsn string
if d.Type == "mysql" { if d.Type == entity.DbTypeMysql {
dsn = fmt.Sprintf("%s:%s@%s(%s:%d)/%s?timeout=8s", d.Username, d.Password, d.Network, d.Host, d.Port, d.Database) dsn = fmt.Sprintf("%s:%s@%s(%s:%d)/%s?timeout=8s", d.Username, d.Password, d.Network, d.Host, d.Port, d.Database)
if d.Params != "" { if d.Params != "" {
dsn = fmt.Sprintf("%s&%s", dsn, d.Params) dsn = fmt.Sprintf("%s&%s", dsn, d.Params)
@@ -373,7 +418,7 @@ func getDsn(d *entity.Db) string {
return dsn return dsn
} }
if d.Type == "postgres" { if d.Type == entity.DbTypePostgres {
dsn = fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", d.Host, d.Port, d.Username, d.Password, d.Database) dsn = fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", d.Host, d.Port, d.Username, d.Password, d.Database)
if d.Params != "" { if d.Params != "" {
dsn = fmt.Sprintf("%s %s", dsn, strings.Join(strings.Split(d.Params, "&"), " ")) dsn = fmt.Sprintf("%s %s", dsn, strings.Join(strings.Split(d.Params, "&"), " "))
@@ -469,7 +514,7 @@ const (
func (d *DbInstance) GetTableMetedatas() []map[string]interface{} { func (d *DbInstance) GetTableMetedatas() []map[string]interface{} {
var sql string var sql string
if d.Type == "mysql" { if d.Type == entity.DbTypeMysql {
sql = MYSQL_TABLE_MA sql = MYSQL_TABLE_MA
} else if d.Type == "postgres" { } else if d.Type == "postgres" {
sql = PGSQL_TABLE_MA sql = PGSQL_TABLE_MA
@@ -489,10 +534,10 @@ func (d *DbInstance) GetColumnMetadatas(tableNames ...string) []map[string]inter
var countSqlTmp string var countSqlTmp string
var sqlTmp string var sqlTmp string
if d.Type == "mysql" { if d.Type == entity.DbTypeMysql {
countSqlTmp = MYSQL_COLOUMN_MA_COUNT countSqlTmp = MYSQL_COLOUMN_MA_COUNT
sqlTmp = MYSQL_COLUMN_MA sqlTmp = MYSQL_COLUMN_MA
} else if d.Type == "postgres" { } else if d.Type == entity.DbTypePostgres {
countSqlTmp = PGSQL_COLUMN_MA_COUNT countSqlTmp = PGSQL_COLUMN_MA_COUNT
sqlTmp = PGSQL_COLUMN_MA sqlTmp = PGSQL_COLUMN_MA
} }
@@ -524,9 +569,9 @@ func (d *DbInstance) GetPrimaryKey(tablename string) string {
func (d *DbInstance) GetTableInfos() []map[string]interface{} { func (d *DbInstance) GetTableInfos() []map[string]interface{} {
var sql string var sql string
if d.Type == "mysql" { if d.Type == entity.DbTypeMysql {
sql = MYSQL_TABLE_INFO sql = MYSQL_TABLE_INFO
} else if d.Type == "postgres" { } else if d.Type == entity.DbTypePostgres {
sql = PGSQL_TABLE_INFO sql = PGSQL_TABLE_INFO
} }
_, res, _ := d.SelectData(sql) _, res, _ := d.SelectData(sql)
@@ -535,9 +580,9 @@ func (d *DbInstance) GetTableInfos() []map[string]interface{} {
func (d *DbInstance) GetTableIndex(tableName string) []map[string]interface{} { func (d *DbInstance) GetTableIndex(tableName string) []map[string]interface{} {
var sql string var sql string
if d.Type == "mysql" { if d.Type == entity.DbTypeMysql {
sql = fmt.Sprintf(MYSQL_INDEX_INFO, tableName) sql = fmt.Sprintf(MYSQL_INDEX_INFO, tableName)
} else if d.Type == "postgres" { } else if d.Type == entity.DbTypePostgres {
sql = fmt.Sprintf(PGSQL_INDEX_INFO, tableName) sql = fmt.Sprintf(PGSQL_INDEX_INFO, tableName)
} }
_, res, _ := d.SelectData(sql) _, res, _ := d.SelectData(sql)
@@ -546,7 +591,7 @@ func (d *DbInstance) GetTableIndex(tableName string) []map[string]interface{} {
func (d *DbInstance) GetCreateTableDdl(tableName string) []map[string]interface{} { func (d *DbInstance) GetCreateTableDdl(tableName string) []map[string]interface{} {
var sql string var sql string
if d.Type == "mysql" { if d.Type == entity.DbTypeMysql {
sql = fmt.Sprintf("show create table %s ", tableName) sql = fmt.Sprintf("show create table %s ", tableName)
} }
_, res, _ := d.SelectData(sql) _, res, _ := d.SelectData(sql)

View File

@@ -4,15 +4,19 @@ import (
"context" "context"
"mayfly-go/internal/devops/domain/entity" "mayfly-go/internal/devops/domain/entity"
"mayfly-go/internal/devops/domain/repository" "mayfly-go/internal/devops/domain/repository"
"mayfly-go/internal/devops/infrastructure/machine"
"mayfly-go/internal/devops/infrastructure/persistence" "mayfly-go/internal/devops/infrastructure/persistence"
"mayfly-go/pkg/biz" "mayfly-go/pkg/biz"
"mayfly-go/pkg/cache" "mayfly-go/pkg/cache"
"mayfly-go/pkg/global" "mayfly-go/pkg/global"
"mayfly-go/pkg/model" "mayfly-go/pkg/model"
"mayfly-go/pkg/utils"
"net"
"time" "time"
"go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/options"
"golang.org/x/crypto/ssh"
) )
type Mongo interface { type Mongo interface {
@@ -80,13 +84,13 @@ func (d *mongoAppImpl) Save(m *entity.Mongo) {
} }
func (d *mongoAppImpl) GetMongoCli(id uint64) *mongo.Client { func (d *mongoAppImpl) GetMongoCli(id uint64) *mongo.Client {
cli, err := GetMongoCli(id, func(u uint64) string { mongoInstance, err := GetMongoInstance(id, func(u uint64) *entity.Mongo {
mongo := d.GetById(id) mongo := d.GetById(u)
biz.NotNil(mongo, "mongo信息不存在") biz.NotNil(mongo, "mongo信息不存在")
return mongo.Uri return mongo
}) })
biz.ErrIsNilAppendErr(err, "连接mongo失败: %s") biz.ErrIsNilAppendErr(err, "连接mongo失败: %s")
return cli return mongoInstance.Cli
} }
// ----------------------------------------------------------- // -----------------------------------------------------------
@@ -95,21 +99,22 @@ func (d *mongoAppImpl) GetMongoCli(id uint64) *mongo.Client {
var mongoCliCache = cache.NewTimedCache(30*time.Minute, 5*time.Second). var mongoCliCache = cache.NewTimedCache(30*time.Minute, 5*time.Second).
WithUpdateAccessTime(true). WithUpdateAccessTime(true).
OnEvicted(func(key interface{}, value interface{}) { OnEvicted(func(key interface{}, value interface{}) {
global.Log.Info("关闭mongo连接: id = ", key) global.Log.Info("删除mongo连接缓存: id = ", key)
value.(*mongo.Client).Disconnect(context.TODO()) value.(*MongoInstance).Close()
}) })
func GetMongoCli(mongoId uint64, getMongoUri func(uint64) string) (*mongo.Client, error) { // 获取mongo的连接实例
cli, err := mongoCliCache.ComputeIfAbsent(mongoId, func(key interface{}) (interface{}, error) { func GetMongoInstance(mongoId uint64, getMongoEntity func(uint64) *entity.Mongo) (*MongoInstance, error) {
c, err := connect(getMongoUri(mongoId)) mi, err := mongoCliCache.ComputeIfAbsent(mongoId, func(_ interface{}) (interface{}, error) {
c, err := connect(getMongoEntity(mongoId))
if err != nil { if err != nil {
return nil, err return nil, err
} }
return c, nil return c, nil
}) })
if cli != nil { if mi != nil {
return cli.(*mongo.Client), err return mi.(*MongoInstance), err
} }
return nil, err return nil, err
} }
@@ -118,16 +123,67 @@ func DeleteMongoCache(mongoId uint64) {
mongoCliCache.Delete(mongoId) mongoCliCache.Delete(mongoId)
} }
type MongoInstance struct {
Id uint64
ProjectId uint64
Cli *mongo.Client
sshTunnel *ssh.Client
}
func (mi *MongoInstance) Close() {
if mi.Cli != nil {
if err := mi.Cli.Disconnect(context.Background()); err != nil {
global.Log.Errorf("关闭mongo实例[%d]连接失败: %s", mi.Id, err)
}
}
if mi.sshTunnel != nil {
if err := mi.sshTunnel.Close(); err != nil {
global.Log.Errorf("关闭mongo实例[%d]的ssh隧道失败: %s", mi.Id, err.Error())
}
}
}
// 连接mongo并返回client // 连接mongo并返回client
func connect(uri string) (*mongo.Client, error) { func connect(me *entity.Mongo) (*MongoInstance, error) {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel() defer cancel()
client, err := mongo.Connect(ctx, options.Client().ApplyURI(uri).SetMaxPoolSize(2))
mongoInstance := &MongoInstance{Id: me.Id, ProjectId: me.ProjectId}
mongoOptions := options.Client().ApplyURI(me.Uri).
SetMaxPoolSize(1)
// 启用ssh隧道则连接隧道机器
if me.EnableSshTunnel == 1 {
machineEntity := MachineApp.GetById(4)
sshClient, err := machine.GetSshClient(machineEntity)
biz.ErrIsNilAppendErr(err, "ssh隧道连接失败: %s")
mongoInstance.sshTunnel = sshClient
mongoOptions.SetDialer(&MongoSshDialer{sshTunnel: sshClient})
}
client, err := mongo.Connect(ctx, mongoOptions)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err = client.Ping(context.TODO(), nil); err != nil { if err = client.Ping(context.TODO(), nil); err != nil {
return nil, err return nil, err
} }
return client, err
global.Log.Infof("连接mongo: %s", me.Uri)
mongoInstance.Cli = client
return mongoInstance, err
}
type MongoSshDialer struct {
sshTunnel *ssh.Client
}
func (sd *MongoSshDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
if sshConn, err := sd.sshTunnel.Dial(network, address); err == nil {
// 将ssh conn包装否则内部部设置超时会报错,ssh conn不支持设置超时会返回错误: ssh: tcpChan: deadline not supported
return &utils.WrapSshConn{Conn: sshConn}, nil
} else {
return nil, err
}
} }

View File

@@ -5,15 +5,19 @@ import (
"fmt" "fmt"
"mayfly-go/internal/devops/domain/entity" "mayfly-go/internal/devops/domain/entity"
"mayfly-go/internal/devops/domain/repository" "mayfly-go/internal/devops/domain/repository"
"mayfly-go/internal/devops/infrastructure/machine"
"mayfly-go/internal/devops/infrastructure/persistence" "mayfly-go/internal/devops/infrastructure/persistence"
"mayfly-go/pkg/biz" "mayfly-go/pkg/biz"
"mayfly-go/pkg/cache" "mayfly-go/pkg/cache"
"mayfly-go/pkg/global" "mayfly-go/pkg/global"
"mayfly-go/pkg/model" "mayfly-go/pkg/model"
"mayfly-go/pkg/utils"
"net"
"strings" "strings"
"time" "time"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
"golang.org/x/crypto/ssh"
) )
type Redis interface { type Redis interface {
@@ -109,25 +113,23 @@ func (r *redisAppImpl) GetRedisInstance(id uint64) *RedisInstance {
biz.NotNil(re, "redis信息不存在") biz.NotNil(re, "redis信息不存在")
redisMode := re.Mode redisMode := re.Mode
ri := &RedisInstance{Id: id, ProjectId: re.ProjectId, Mode: redisMode} var ri *RedisInstance
if redisMode == "" || redisMode == entity.RedisModeStandalone { if redisMode == "" || redisMode == entity.RedisModeStandalone {
rcli := getRedisCient(re) ri = getRedisCient(re)
// 测试连接 // 测试连接
_, e := rcli.Ping(context.Background()).Result() _, e := ri.Cli.Ping(context.Background()).Result()
if e != nil { if e != nil {
rcli.Close() ri.Close()
panic(biz.NewBizErr(fmt.Sprintf("redis连接失败: %s", e.Error()))) panic(biz.NewBizErr(fmt.Sprintf("redis连接失败: %s", e.Error())))
} }
ri.Cli = rcli
} else if redisMode == entity.RedisModeCluster { } else if redisMode == entity.RedisModeCluster {
ccli := getRedisClusterClient(re) ri = getRedisClusterClient(re)
// 测试连接 // 测试连接
_, e := ccli.Ping(context.Background()).Result() _, e := ri.ClusterCli.Ping(context.Background()).Result()
if e != nil { if e != nil {
ccli.Close() ri.Close()
panic(biz.NewBizErr(fmt.Sprintf("redis集群连接失败: %s", e.Error()))) panic(biz.NewBizErr(fmt.Sprintf("redis集群连接失败: %s", e.Error())))
} }
ri.ClusterCli = ccli
} }
global.Log.Infof("连接redis: %s", re.Host) global.Log.Infof("连接redis: %s", re.Host)
@@ -137,21 +139,56 @@ func (r *redisAppImpl) GetRedisInstance(id uint64) *RedisInstance {
return ri return ri
} }
func getRedisCient(re *entity.Redis) *redis.Client { func getRedisCient(re *entity.Redis) *RedisInstance {
return redis.NewClient(&redis.Options{ ri := &RedisInstance{Id: re.Id, ProjectId: re.ProjectId, Mode: re.Mode}
Addr: re.Host,
Password: re.Password, // no password set redisOptions := &redis.Options{
DB: re.Db, // use default DB Addr: re.Host,
DialTimeout: 8 * time.Second, Password: re.Password, // no password set
}) DB: re.Db, // use default DB
DialTimeout: 8 * time.Second,
ReadTimeout: -1, // Disable timeouts, because SSH does not support deadlines.
WriteTimeout: -1,
}
if re.EnableSshTunnel == 1 {
sshClient, dialerFunc := getRedisDialer(re.SshTunnelMachineId)
ri.sshTunnel = sshClient
redisOptions.Dialer = dialerFunc
}
ri.Cli = redis.NewClient(redisOptions)
return ri
} }
func getRedisClusterClient(re *entity.Redis) *redis.ClusterClient { func getRedisClusterClient(re *entity.Redis) *RedisInstance {
return redis.NewClusterClient(&redis.ClusterOptions{ ri := &RedisInstance{Id: re.Id, ProjectId: re.ProjectId, Mode: re.Mode}
redisClusterOptions := &redis.ClusterOptions{
Addrs: strings.Split(re.Host, ","), Addrs: strings.Split(re.Host, ","),
Password: re.Password, Password: re.Password,
DialTimeout: 8 * time.Second, DialTimeout: 8 * time.Second,
}) }
if re.EnableSshTunnel == 1 {
sshClient, dialerFunc := getRedisDialer(re.SshTunnelMachineId)
ri.sshTunnel = sshClient
redisClusterOptions.Dialer = dialerFunc
}
ri.ClusterCli = redis.NewClusterClient(redisClusterOptions)
return ri
}
func getRedisDialer(machineId uint64) (*ssh.Client, func(ctx context.Context, network, addr string) (net.Conn, error)) {
me := MachineApp.GetById(machineId)
sshClient, err := machine.GetSshClient(me)
biz.ErrIsNilAppendErr(err, "ssh隧道连接失败: %s")
return sshClient, func(_ context.Context, network, addr string) (net.Conn, error) {
if sshConn, err := sshClient.Dial(network, addr); err == nil {
// 将ssh conn包装否则redis内部设置超时会报错,ssh conn不支持设置超时会返回错误: ssh: tcpChan: deadline not supported
return &utils.WrapSshConn{Conn: sshConn}, nil
} else {
return nil, err
}
}
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -174,11 +211,11 @@ func TestRedisConnection(re *entity.Redis) {
if re.Mode == "" || re.Mode == entity.RedisModeStandalone { if re.Mode == "" || re.Mode == entity.RedisModeStandalone {
rcli := getRedisCient(re) rcli := getRedisCient(re)
defer rcli.Close() defer rcli.Close()
cmd = rcli cmd = rcli.Cli
} else if re.Mode == entity.RedisModeCluster { } else if re.Mode == entity.RedisModeCluster {
ccli := getRedisClusterClient(re) ccli := getRedisClusterClient(re)
defer ccli.Close() defer ccli.Close()
cmd = ccli cmd = ccli.ClusterCli
} }
// 测试连接 // 测试连接
@@ -193,6 +230,7 @@ type RedisInstance struct {
Mode string Mode string
Cli *redis.Client Cli *redis.Client
ClusterCli *redis.ClusterClient ClusterCli *redis.ClusterClient
sshTunnel *ssh.Client
} }
// 获取命令执行接口的具体实现 // 获取命令执行接口的具体实现
@@ -215,10 +253,18 @@ func (r *RedisInstance) Scan(cursor uint64, match string, count int64) ([]string
func (r *RedisInstance) Close() { func (r *RedisInstance) Close() {
if r.Mode == entity.RedisModeStandalone { if r.Mode == entity.RedisModeStandalone {
r.Cli.Close() if err := r.Cli.Close(); err != nil {
return global.Log.Errorf("关闭redis单机实例[%d]连接失败: %s", r.Id, err.Error())
}
} }
if r.Mode == entity.RedisModeCluster { if r.Mode == entity.RedisModeCluster {
r.ClusterCli.Close() if err := r.ClusterCli.Close(); err != nil {
global.Log.Errorf("关闭redis集群实例[%d]连接失败: %s", r.Id, err.Error())
}
}
if r.sshTunnel != nil {
if err := r.sshTunnel.Close(); err != nil {
global.Log.Errorf("关闭redis实例[%d]的ssh隧道失败: %s", r.Id, err.Error())
}
} }
} }

View File

@@ -1,6 +1,7 @@
package entity package entity
import ( import (
"fmt"
"mayfly-go/pkg/model" "mayfly-go/pkg/model"
) )
@@ -21,9 +22,24 @@ type Db struct {
EnvId uint64 EnvId uint64
Env string Env string
EnableSSH int `orm:"column(enable_ssh)" json:"enable_ssh"` EnableSshTunnel int8 `orm:"column(enable_ssh_tunnel)" json:"enableSshTunnel"` // 是否启用ssh隧道
SSHHost string `orm:"column(ssh_host)" json:"ssh_host"` SshTunnelMachineId uint64 `orm:"column(ssh_tunnel_machine_id)" json:"sshTunnelMachineId"` // ssh隧道机器id
SSHPort int `orm:"column(ssh_port)" json:"ssh_port"`
SSHUser string `orm:"column(ssh_user)" json:"ssh_user"`
SSHPass string `orm:"column(ssh_pass)" json:"-"`
} }
// 获取数据库连接网络, 若没有使用ssh隧道则直接返回。否则返回拼接的网络需要注册至指定dial
func (d Db) GetNetwork() string {
network := d.Network
if d.EnableSshTunnel == -1 {
if network == "" {
return "tcp"
} else {
return network
}
}
return fmt.Sprintf("%s+ssh:%d", d.Type, d.SshTunnelMachineId)
}
const (
DbTypeMysql = "mysql"
DbTypePostgres = "postgres"
)

View File

@@ -9,8 +9,9 @@ type Machine struct {
ProjectId uint64 `json:"projectId"` ProjectId uint64 `json:"projectId"`
ProjectName string `json:"projectName"` ProjectName string `json:"projectName"`
Name string `json:"name"` Name string `json:"name"`
Ip string `json:"ip"` // IP地址 Ip string `json:"ip"` // IP地址
Username string `json:"username"` // 用户名 Username string `json:"username"` // 用户名
AuthMethod int8 `json:"authMethod"` // 授权认证方式
Password string `json:"-"` Password string `json:"-"`
Port int `json:"port"` // 端口号 Port int `json:"port"` // 端口号
Status int8 `json:"status"` // 状态 1:启用2:停用 Status int8 `json:"status"` // 状态 1:启用2:停用
@@ -18,6 +19,8 @@ type Machine struct {
} }
const ( const (
MachineStatusEnable int8 = 1 // 启用状态 MachineStatusEnable int8 = 1 // 启用状态
MachineStatusDisable int8 = -1 // 禁用状态 MachineStatusDisable int8 = -1 // 禁用状态
MachineAuthMethodPassword int8 = 1 // 密码登录
MachineAuthMethodPublicKey int8 = 2 // 公钥免密登录
) )

View File

@@ -5,10 +5,12 @@ import "mayfly-go/pkg/model"
type Mongo struct { type Mongo struct {
model.Model model.Model
Name string `orm:"column(name)" json:"name"` Name string `orm:"column(name)" json:"name"`
Uri string `orm:"column(uri)" json:"uri"` Uri string `orm:"column(uri)" json:"uri"`
ProjectId uint64 `json:"projectId"` EnableSshTunnel int8 `orm:"column(enable_ssh_tunnel)" json:"enableSshTunnel"` // 是否启用ssh隧道
Project string `json:"project"` SshTunnelMachineId uint64 `orm:"column(ssh_tunnel_machine_id)" json:"sshTunnelMachineId"` // ssh隧道机器id
EnvId uint64 `json:"envId"` ProjectId uint64 `json:"projectId"`
Env string `json:"env"` Project string `json:"project"`
EnvId uint64 `json:"envId"`
Env string `json:"env"`
} }

View File

@@ -7,15 +7,17 @@ import (
type Redis struct { type Redis struct {
model.Model model.Model
Host string `orm:"column(host)" json:"host"` Host string `orm:"column(host)" json:"host"`
Mode string `json:"mode"` Mode string `json:"mode"`
Password string `orm:"column(password)" json:"-"` Password string `orm:"column(password)" json:"-"`
Db int `orm:"column(database)" json:"db"` Db int `orm:"column(database)" json:"db"`
Remark string EnableSshTunnel int8 `orm:"column(enable_ssh_tunnel)" json:"enableSshTunnel"` // 是否启用ssh隧道
ProjectId uint64 SshTunnelMachineId uint64 `orm:"column(ssh_tunnel_machine_id)" json:"sshTunnelMachineId"` // ssh隧道机器id
Project string Remark string
EnvId uint64 ProjectId uint64
Env string Project string
EnvId uint64
Env string
} }
const ( const (

View File

@@ -24,58 +24,6 @@ type Cli struct {
sftpClient *sftp.Client sftpClient *sftp.Client
} }
// 机器客户端连接缓存45分钟内没有访问则会被关闭
var cliCache = cache.NewTimedCache(45*time.Minute, 5*time.Second).
WithUpdateAccessTime(true).
OnEvicted(func(key interface{}, value interface{}) {
value.(*Cli).Close()
})
// 是否存在指定id的客户端连接
func HasCli(machineId uint64) bool {
if _, ok := cliCache.Get(machineId); ok {
return true
}
return false
}
// 删除指定机器客户端,并关闭客户端连接
func DeleteCli(id uint64) {
cliCache.Delete(id)
}
// 从缓存中获取客户端信息,不存在则回调获取机器信息函数,并新建
func GetCli(machineId uint64, getMachine func(uint64) *entity.Machine) (*Cli, error) {
cli, err := cliCache.ComputeIfAbsent(machineId, func(key interface{}) (interface{}, error) {
c, err := newClient(getMachine(machineId))
if err != nil {
return nil, err
}
return c, nil
})
if cli != nil {
return cli.(*Cli), err
}
return nil, err
}
//根据机器信息创建客户端对象
func newClient(machine *entity.Machine) (*Cli, error) {
if machine == nil {
return nil, errors.New("机器不存在")
}
global.Log.Infof("[%s]机器连接:%s:%d", machine.Name, machine.Ip, machine.Port)
cli := new(Cli)
cli.machine = machine
err := cli.connect()
if err != nil {
return nil, err
}
return cli, nil
}
//连接 //连接
func (c *Cli) connect() error { func (c *Cli) connect() error {
// 如果已经有client则直接返回 // 如果已经有client则直接返回
@@ -83,16 +31,7 @@ func (c *Cli) connect() error {
return nil return nil
} }
m := c.machine m := c.machine
config := ssh.ClientConfig{ sshClient, err := GetSshClient(m)
User: m.Username,
Auth: []ssh.AuthMethod{ssh.Password(m.Password)},
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
Timeout: 5 * time.Second,
}
addr := fmt.Sprintf("%s:%d", m.Ip, m.Port)
sshClient, err := ssh.Dial("tcp", addr, &config)
if err != nil { if err != nil {
return err return err
} }
@@ -100,25 +39,6 @@ func (c *Cli) connect() error {
return nil return nil
} }
// 测试连接
func TestConn(m *entity.Machine) error {
config := ssh.ClientConfig{
User: m.Username,
Auth: []ssh.AuthMethod{ssh.Password(m.Password)},
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
Timeout: 5 * time.Second,
}
addr := fmt.Sprintf("%s:%d", m.Ip, m.Port)
sshClient, err := ssh.Dial("tcp", addr, &config)
if err != nil {
return err
}
defer sshClient.Close()
return nil
}
// 关闭client和并从缓存中移除 // 关闭client和并从缓存中移除
func (c *Cli) Close() { func (c *Cli) Close() {
m := c.machine m := c.machine
@@ -184,3 +104,91 @@ func (c *Cli) Run(shell string) (*string, error) {
func (c *Cli) GetMachine() *entity.Machine { func (c *Cli) GetMachine() *entity.Machine {
return c.machine return c.machine
} }
// 机器客户端连接缓存45分钟内没有访问则会被关闭
var cliCache = cache.NewTimedCache(45*time.Minute, 5*time.Second).
WithUpdateAccessTime(true).
OnEvicted(func(_, value interface{}) {
value.(*Cli).Close()
})
// 是否存在指定id的客户端连接
func HasCli(machineId uint64) bool {
if _, ok := cliCache.Get(machineId); ok {
return true
}
return false
}
// 删除指定机器客户端,并关闭客户端连接
func DeleteCli(id uint64) {
cliCache.Delete(id)
}
// 从缓存中获取客户端信息,不存在则回调获取机器信息函数,并新建
func GetCli(machineId uint64, getMachine func(uint64) *entity.Machine) (*Cli, error) {
cli, err := cliCache.ComputeIfAbsent(machineId, func(_ interface{}) (interface{}, error) {
c, err := newClient(getMachine(machineId))
if err != nil {
return nil, err
}
return c, nil
})
if cli != nil {
return cli.(*Cli), err
}
return nil, err
}
// 测试连接
func TestConn(m *entity.Machine) error {
sshClient, err := GetSshClient(m)
if err != nil {
return err
}
defer sshClient.Close()
return nil
}
func GetSshClient(m *entity.Machine) (*ssh.Client, error) {
config := ssh.ClientConfig{
User: m.Username,
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
Timeout: 5 * time.Second,
}
if m.AuthMethod == entity.MachineAuthMethodPassword {
config.Auth = []ssh.AuthMethod{ssh.Password(m.Password)}
} else if m.AuthMethod == entity.MachineAuthMethodPublicKey {
if signer, err := ssh.ParsePrivateKey([]byte(m.Password)); err != nil {
return nil, err
} else {
config.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)}
}
}
addr := fmt.Sprintf("%s:%d", m.Ip, m.Port)
sshClient, err := ssh.Dial("tcp", addr, &config)
if err != nil {
return nil, err
}
return sshClient, nil
}
//根据机器信息创建客户端对象
func newClient(machine *entity.Machine) (*Cli, error) {
if machine == nil {
return nil, errors.New("机器不存在")
}
global.Log.Infof("[%s]机器连接:%s:%d", machine.Name, machine.Ip, machine.Port)
cli := new(Cli)
cli.machine = machine
err := cli.connect()
if err != nil {
return nil, err
}
return cli, nil
}

View File

@@ -30,6 +30,8 @@ CREATE TABLE `t_db` (
`database` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '数据库,空格分割多个数据库', `database` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '数据库,空格分割多个数据库',
`params` varchar(125) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '其他连接参数', `params` varchar(125) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '其他连接参数',
`network` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL, `network` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
`enableSshTunnel` tinyint(2) DEFAULT NULL COMMENT '是否启用ssh隧道',
`sshTunnelMachineId` bigint(20) DEFAULT NULL COMMENT 'ssh隧道的机器id',
`project_id` bigint(20) DEFAULT NULL, `project_id` bigint(20) DEFAULT NULL,
`project` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL, `project` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL,
`env_id` bigint(20) DEFAULT NULL COMMENT '环境id', `env_id` bigint(20) DEFAULT NULL COMMENT '环境id',
@@ -41,11 +43,6 @@ CREATE TABLE `t_db` (
`update_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL,
`modifier_id` bigint(20) DEFAULT NULL, `modifier_id` bigint(20) DEFAULT NULL,
`modifier` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL, `modifier` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL,
`enable_ssh` tinyint(1) unsigned NOT NULL DEFAULT '0',
`ssh_host` varchar(50) COLLATE utf8mb4_bin NOT NULL DEFAULT '',
`ssh_port` int(8) NOT NULL,
`ssh_user` varchar(255) COLLATE utf8mb4_bin NOT NULL DEFAULT '',
`ssh_pass` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='数据库资源信息表'; ) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='数据库资源信息表';
@@ -112,7 +109,8 @@ CREATE TABLE `t_machine` (
`ip` varchar(36) COLLATE utf8mb4_bin NOT NULL, `ip` varchar(36) COLLATE utf8mb4_bin NOT NULL,
`port` int(12) NOT NULL, `port` int(12) NOT NULL,
`username` varchar(12) COLLATE utf8mb4_bin NOT NULL, `username` varchar(12) COLLATE utf8mb4_bin NOT NULL,
`password` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL, `auth_method` tinyint(2) NULL DEFAULT NULL COMMENT '1.密码登录2.publickey登录',
`password` varchar(3200) COLLATE utf8mb4_bin DEFAULT NULL,
`status` tinyint(2) NOT NULL COMMENT '状态: 1:启用; -1:禁用', `status` tinyint(2) NOT NULL COMMENT '状态: 1:启用; -1:禁用',
`remark` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, `remark` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
`need_monitor` tinyint(2) DEFAULT NULL, `need_monitor` tinyint(2) DEFAULT NULL,
@@ -263,6 +261,8 @@ CREATE TABLE `t_redis` (
`password` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL, `password` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL,
`db` int(32) DEFAULT NULL, `db` int(32) DEFAULT NULL,
`mode` varchar(32) DEFAULT NULL, `mode` varchar(32) DEFAULT NULL,
`enableSshTunnel` tinyint(2) DEFAULT NULL COMMENT '是否启用ssh隧道',
`sshTunnelMachineId` bigint(20) DEFAULT NULL COMMENT 'ssh隧道的机器id',
`remark` varchar(125) DEFAULT NULL, `remark` varchar(125) DEFAULT NULL,
`project_id` bigint(20) DEFAULT NULL, `project_id` bigint(20) DEFAULT NULL,
`project` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL, `project` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL,
@@ -668,6 +668,8 @@ CREATE TABLE `t_mongo` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(36) COLLATE utf8mb4_bin NOT NULL COMMENT '名称', `name` varchar(36) COLLATE utf8mb4_bin NOT NULL COMMENT '名称',
`uri` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '连接uri', `uri` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '连接uri',
`enableSshTunnel` tinyint(2) DEFAULT NULL COMMENT '是否启用ssh隧道',
`sshTunnelMachineId` bigint(20) DEFAULT NULL COMMENT 'ssh隧道的机器id',
`project_id` bigint(20) NOT NULL, `project_id` bigint(20) NOT NULL,
`project` varchar(36) COLLATE utf8mb4_bin DEFAULT NULL, `project` varchar(36) COLLATE utf8mb4_bin DEFAULT NULL,
`env_id` bigint(20) DEFAULT NULL, `env_id` bigint(20) DEFAULT NULL,

View File

@@ -0,0 +1,36 @@
package utils
import (
"net"
"time"
)
type WrapSshConn struct {
Conn net.Conn
}
func (c *WrapSshConn) Read(b []byte) (n int, err error) {
return c.Conn.Read(b)
}
func (c *WrapSshConn) Write(b []byte) (n int, err error) {
return c.Conn.Write(b)
}
func (c *WrapSshConn) Close() error {
return c.Conn.Close()
}
func (c *WrapSshConn) LocalAddr() net.Addr {
return c.Conn.LocalAddr()
}
func (c *WrapSshConn) RemoteAddr() net.Addr {
return c.Conn.RemoteAddr()
}
func (c *WrapSshConn) SetDeadline(t time.Time) error {
return c.Conn.SetDeadline(t)
}
func (c *WrapSshConn) SetReadDeadline(t time.Time) error {
return nil
}
func (c *WrapSshConn) SetWriteDeadline(t time.Time) error {
return nil
}