fix SH配置和节点配置新增跨工作空间同步功能,方便快速同步信息 #I56YTU

This commit is contained in:
bwcx_jzy 2022-05-12 14:39:06 +08:00
parent 79a311afce
commit 4e3dd39528
No known key found for this signature in database
GPG Key ID: 5E48E9372088B9E5
13 changed files with 563 additions and 238 deletions

View File

@ -7,7 +7,8 @@
1. 【server】节点新增代理配置实现使用代理访问插件端感谢@背着砍刀的诗人)
2. 【server】构建新增差异构建配置选择如果仓库代码未变动则不执行构建
3. 项目管理文件新增备份,自动备份变动的文件(感谢[@少爷123](https://gitee.com/58753101) [Gitee issues I54ZFM](https://gitee.com/dromara/Jpom/issues/I54ZFM)
3. 项目管理文件新增备份,自动备份变动的文件(感谢[@少爷123](https://gitee.com/58753101) [Gitee issues I54ZFM](https://gitee.com/dromara/Jpom/issues/I54ZFM)
4. 【server】SH配置和节点配置新增跨工作空间同步功能方便快速同步信息感谢[@陈旭](https://gitee.com/chenxu8989) [Gitee issues I56YTU](https://gitee.com/dromara/Jpom/issues/I56YTU)
### 🐞 解决BUG、优化功能
@ -18,6 +19,7 @@
5. 【server】调整 docker-compose 使用卷方式存储数据,避免在部分环境中出现无法正常使用情况 (感谢 [@💎ℳ๓₯㎕斌💎💘](https://gitee.com/weihongbin) 贡献解决方案)(感谢[@笨笨巫师](https://gitee.com/zhangxin_gitosc) [Gitee issues I52OAV](https://gitee.com/dromara/Jpom/issues/I52OAV)
6. 【server】调整节点里面在部分情况下会出现空白行 (感谢[@💎ℳ๓₯㎕斌💎💘](https://gitee.com/weihongbin)
7. 【server】前端部分输入框添加`maxLength` 限制避免出现数据库字段长度不足问题(感谢@ccx2480
8. 【agent】修复项目下载远程文件解压方法错误感谢@背着砍刀的诗人
> 使用项目文件备份说明:
>

View File

@ -217,11 +217,37 @@ public class NodeEditController extends BaseServerController {
return JsonMessage.getString(200, "操作成功");
}
/**
* 解锁节点通过插件端自动注册的节点默认未分配工作空间
*
* @param id 节点ID
* @param workspaceId 分配到到工作空间ID
* @return msg
*/
@GetMapping(value = "un_lock_workspace", produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(method = MethodFeature.EDIT)
@SystemPermission(superUser = true)
@SystemPermission()
public String unLockWorkspace(@ValidatorItem String id, @ValidatorItem String workspaceId) {
nodeService.checkUserWorkspace(workspaceId);
nodeService.unLock(id, workspaceId);
return JsonMessage.getString(200, "操作成功");
}
/**
* 同步到指定工作空间
*
* @param ids 节点ID
* @param workspaceId 分配到到工作空间ID
* @return msg
*/
@GetMapping(value = "sync-to-workspace", produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(method = MethodFeature.EDIT)
@SystemPermission()
public String syncToWorkspace(@ValidatorItem String ids, @ValidatorItem String workspaceId) {
String nowWorkspaceId = nodeService.getCheckUserWorkspace(getRequest());
//
nodeService.checkUserWorkspace(workspaceId);
nodeService.syncToWorkspace(ids, nowWorkspaceId, workspaceId);
return JsonMessage.getString(200, "操作成功");
}
}

View File

@ -49,6 +49,7 @@ import io.jpom.model.log.SshTerminalExecuteLog;
import io.jpom.permission.ClassFeature;
import io.jpom.permission.Feature;
import io.jpom.permission.MethodFeature;
import io.jpom.permission.SystemPermission;
import io.jpom.service.dblog.BuildInfoService;
import io.jpom.service.dblog.SshTerminalExecuteLogService;
import io.jpom.service.node.ssh.SshService;
@ -72,214 +73,231 @@ import java.util.List;
@Feature(cls = ClassFeature.SSH)
public class SshController extends BaseServerController {
private final SshService sshService;
private final SshTerminalExecuteLogService sshTerminalExecuteLogService;
private final BuildInfoService buildInfoService;
private final SshService sshService;
private final SshTerminalExecuteLogService sshTerminalExecuteLogService;
private final BuildInfoService buildInfoService;
public SshController(SshService sshService,
SshTerminalExecuteLogService sshTerminalExecuteLogService,
BuildInfoService buildInfoService) {
this.sshService = sshService;
this.sshTerminalExecuteLogService = sshTerminalExecuteLogService;
this.buildInfoService = buildInfoService;
}
public SshController(SshService sshService,
SshTerminalExecuteLogService sshTerminalExecuteLogService,
BuildInfoService buildInfoService) {
this.sshService = sshService;
this.sshTerminalExecuteLogService = sshTerminalExecuteLogService;
this.buildInfoService = buildInfoService;
}
@PostMapping(value = "list_data.json", produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(method = MethodFeature.LIST)
public JsonMessage<PageResultDto<SshModel>> listData() {
PageResultDto<SshModel> pageResultDto = sshService.listPage(getRequest());
return new JsonMessage<>(200, "", pageResultDto);
}
@PostMapping(value = "list_data.json", produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(method = MethodFeature.LIST)
public JsonMessage<PageResultDto<SshModel>> listData() {
PageResultDto<SshModel> pageResultDto = sshService.listPage(getRequest());
return new JsonMessage<>(200, "", pageResultDto);
}
@GetMapping(value = "list_data_all.json", produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(method = MethodFeature.LIST)
public JsonMessage<List<SshModel>> listDataAll() {
List<SshModel> list = sshService.listByWorkspace(getRequest());
return new JsonMessage<>(200, "", list);
}
@GetMapping(value = "list_data_all.json", produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(method = MethodFeature.LIST)
public JsonMessage<List<SshModel>> listDataAll() {
List<SshModel> list = sshService.listByWorkspace(getRequest());
return new JsonMessage<>(200, "", list);
}
/**
* 编辑
*
* @param name 名称
* @param host 端口
* @param user 用户名
* @param password 密码
* @param connectType 连接方式
* @param privateKey 私钥
* @param port 端口
* @param charset 编码格式
* @param fileDirs 文件夹
* @param id ID
* @param notAllowedCommand 禁止输入的命令
* @return json
*/
@PostMapping(value = "save.json", produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(method = MethodFeature.EDIT)
public String save(@ValidatorItem(value = ValidatorRule.NOT_BLANK, msg = "ssh名称不能为空") String name,
@ValidatorItem(value = ValidatorRule.NOT_BLANK, msg = "host不能为空") String host,
@ValidatorItem(value = ValidatorRule.NOT_BLANK, msg = "user不能为空") String user,
String password,
SshModel.ConnectType connectType,
String privateKey,
@ValidatorItem(value = ValidatorRule.POSITIVE_INTEGER, msg = "port错误") int port,
String charset, String fileDirs,
String id, String notAllowedCommand) {
SshModel sshModel;
boolean add = StrUtil.isEmpty(getParameter("id"));
if (add) {
// 优先判断参数 如果是 password 在修改时可以不填写
if (connectType == SshModel.ConnectType.PASS) {
Assert.hasText(password, "请填写登录密码");
} else if (connectType == SshModel.ConnectType.PUBKEY) {
//Assert.hasText(privateKey, "请填写证书内容");
}
sshModel = new SshModel();
} else {
sshModel = sshService.getByKey(id);
Assert.notNull(sshModel, "不存在对应ssh");
}
// 目录
if (StrUtil.isEmpty(fileDirs)) {
sshModel.fileDirs(null);
} else {
List<String> list = StrSplitter.splitTrim(fileDirs, StrUtil.LF, true);
for (String s : list) {
String normalize = FileUtil.normalize(s + StrUtil.SLASH);
int count = StrUtil.count(normalize, StrUtil.SLASH);
Assert.state(count >= 2, "ssh 授权目录不能是根目录");
}
//
UserModel userModel = getUser();
Assert.state(!userModel.isDemoUser(), PermissionInterceptor.DEMO_TIP);
sshModel.fileDirs(list);
}
sshModel.setHost(host);
// 如果密码传递不为空就设置值 因为上面已经判断了只有修改的情况下 password 才可能为空
if (StrUtil.isNotEmpty(password)) {
sshModel.setPassword(password);
}
if (StrUtil.startWith(privateKey, URLUtil.FILE_URL_PREFIX)) {
String rsaPath = StrUtil.removePrefix(privateKey, URLUtil.FILE_URL_PREFIX);
Assert.state(FileUtil.isFile(rsaPath), "配置的私钥文件不存在");
}
if (StrUtil.isNotEmpty(privateKey)) {
sshModel.setPrivateKey(privateKey);
}
/**
* 编辑
*
* @param name 名称
* @param host 端口
* @param user 用户名
* @param password 密码
* @param connectType 连接方式
* @param privateKey 私钥
* @param port 端口
* @param charset 编码格式
* @param fileDirs 文件夹
* @param id ID
* @param notAllowedCommand 禁止输入的命令
* @return json
*/
@PostMapping(value = "save.json", produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(method = MethodFeature.EDIT)
public String save(@ValidatorItem(value = ValidatorRule.NOT_BLANK, msg = "ssh名称不能为空") String name,
@ValidatorItem(value = ValidatorRule.NOT_BLANK, msg = "host不能为空") String host,
@ValidatorItem(value = ValidatorRule.NOT_BLANK, msg = "user不能为空") String user,
String password,
SshModel.ConnectType connectType,
String privateKey,
@ValidatorItem(value = ValidatorRule.POSITIVE_INTEGER, msg = "port错误") int port,
String charset, String fileDirs,
String id, String notAllowedCommand) {
SshModel sshModel;
boolean add = StrUtil.isEmpty(getParameter("id"));
if (add) {
// 优先判断参数 如果是 password 在修改时可以不填写
if (connectType == SshModel.ConnectType.PASS) {
Assert.hasText(password, "请填写登录密码");
} else if (connectType == SshModel.ConnectType.PUBKEY) {
//Assert.hasText(privateKey, "请填写证书内容");
}
sshModel = new SshModel();
} else {
sshModel = sshService.getByKey(id);
Assert.notNull(sshModel, "不存在对应ssh");
}
// 目录
if (StrUtil.isEmpty(fileDirs)) {
sshModel.fileDirs(null);
} else {
List<String> list = StrSplitter.splitTrim(fileDirs, StrUtil.LF, true);
for (String s : list) {
String normalize = FileUtil.normalize(s + StrUtil.SLASH);
int count = StrUtil.count(normalize, StrUtil.SLASH);
Assert.state(count >= 2, "ssh 授权目录不能是根目录");
}
//
UserModel userModel = getUser();
Assert.state(!userModel.isDemoUser(), PermissionInterceptor.DEMO_TIP);
sshModel.fileDirs(list);
}
sshModel.setHost(host);
// 如果密码传递不为空就设置值 因为上面已经判断了只有修改的情况下 password 才可能为空
if (StrUtil.isNotEmpty(password)) {
sshModel.setPassword(password);
}
if (StrUtil.startWith(privateKey, URLUtil.FILE_URL_PREFIX)) {
String rsaPath = StrUtil.removePrefix(privateKey, URLUtil.FILE_URL_PREFIX);
Assert.state(FileUtil.isFile(rsaPath), "配置的私钥文件不存在");
}
if (StrUtil.isNotEmpty(privateKey)) {
sshModel.setPrivateKey(privateKey);
}
sshModel.setPort(port);
sshModel.setUser(user);
sshModel.setName(name);
sshModel.setNotAllowedCommand(notAllowedCommand);
sshModel.setConnectType(connectType.name());
// 获取允许编辑的后缀
String allowEditSuffix = getParameter("allowEditSuffix");
List<String> allowEditSuffixList = AgentWhitelist.parseToList(allowEditSuffix, "允许编辑的文件后缀不能为空");
sshModel.allowEditSuffix(allowEditSuffixList);
try {
Charset.forName(charset);
sshModel.setCharset(charset);
} catch (Exception e) {
return JsonMessage.getString(405, "请填写正确的编码格式");
}
// 判断重复
HttpServletRequest request = getRequest();
String workspaceId = sshService.getCheckUserWorkspace(request);
Entity entity = Entity.create();
entity.set("host", sshModel.getHost());
entity.set("port", sshModel.getPort());
entity.set("workspaceId", workspaceId);
if (StrUtil.isNotEmpty(id)) {
entity.set("id", StrUtil.format(" <> {}", id));
}
boolean exists = sshService.exists(entity);
Assert.state(!exists, "对应的SSH已经存在啦");
try {
SshModel model = sshService.getByKey(id, false);
if (model != null) {
sshModel.setPassword(StrUtil.emptyToDefault(sshModel.getPassword(), model.getPassword()));
sshModel.setPrivateKey(StrUtil.emptyToDefault(sshModel.getPrivateKey(), model.getPrivateKey()));
}
Session session = SshService.getSessionByModel(sshModel);
JschUtil.close(session);
} catch (Exception e) {
return JsonMessage.getString(505, "ssh连接失败" + e.getMessage());
}
if (add) {
sshService.insert(sshModel);
} else {
sshService.update(sshModel);
}
return JsonMessage.getString(200, "操作成功");
}
sshModel.setPort(port);
sshModel.setUser(user);
sshModel.setName(name);
sshModel.setNotAllowedCommand(notAllowedCommand);
sshModel.setConnectType(connectType.name());
// 获取允许编辑的后缀
String allowEditSuffix = getParameter("allowEditSuffix");
List<String> allowEditSuffixList = AgentWhitelist.parseToList(allowEditSuffix, "允许编辑的文件后缀不能为空");
sshModel.allowEditSuffix(allowEditSuffixList);
try {
Charset.forName(charset);
sshModel.setCharset(charset);
} catch (Exception e) {
return JsonMessage.getString(405, "请填写正确的编码格式");
}
// 判断重复
HttpServletRequest request = getRequest();
String workspaceId = sshService.getCheckUserWorkspace(request);
Entity entity = Entity.create();
entity.set("host", sshModel.getHost());
entity.set("port", sshModel.getPort());
entity.set("workspaceId", workspaceId);
if (StrUtil.isNotEmpty(id)) {
entity.set("id", StrUtil.format(" <> {}", id));
}
boolean exists = sshService.exists(entity);
Assert.state(!exists, "对应的SSH已经存在啦");
try {
SshModel model = sshService.getByKey(id, false);
if (model != null) {
sshModel.setPassword(StrUtil.emptyToDefault(sshModel.getPassword(), model.getPassword()));
sshModel.setPrivateKey(StrUtil.emptyToDefault(sshModel.getPrivateKey(), model.getPrivateKey()));
}
Session session = SshService.getSessionByModel(sshModel);
JschUtil.close(session);
} catch (Exception e) {
return JsonMessage.getString(505, "ssh连接失败" + e.getMessage());
}
if (add) {
sshService.insert(sshModel);
} else {
sshService.update(sshModel);
}
return JsonMessage.getString(200, "操作成功");
}
/**
* 检查 ssh 是否安装插件端
*
* @param ids ids
* @return json
*/
@GetMapping(value = "check_agent.json", produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(method = MethodFeature.LIST)
public String checkAgent(String ids) {
List<SshModel> sshModels = sshService.listById(StrUtil.split(ids, StrUtil.COMMA), getRequest());
Assert.notEmpty(sshModels, "没有任何节点信息");
/**
* 检查 ssh 是否安装插件端
*
* @param ids ids
* @return json
*/
@GetMapping(value = "check_agent.json", produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(method = MethodFeature.LIST)
public String checkAgent(String ids) {
List<SshModel> sshModels = sshService.listById(StrUtil.split(ids, StrUtil.COMMA), getRequest());
Assert.notEmpty(sshModels, "没有任何节点信息");
JSONObject result = new JSONObject();
for (SshModel sshModel : sshModels) {
List<NodeModel> nodeBySshId = nodeService.getNodeBySshId(sshModel.getId());
JSONObject data = new JSONObject();
NodeModel nodeModel = CollUtil.getFirst(nodeBySshId);
SshModel model = sshService.getByKey(sshModel.getId(), false);
try {
if (nodeModel == null) {
Integer pid = sshService.checkSshRunPid(model, Type.Agent.getTag());
data.put("pid", ObjectUtil.defaultIfNull(pid, 0));
data.put("ok", true);
} else {
data.put("nodeId", nodeModel.getId());
data.put("nodeName", nodeModel.getName());
}
//
String javaVersion = sshService.checkCommand(model, "java");
data.put("javaVersion", javaVersion);
} catch (Exception e) {
DefaultSystemLog.getLog().error("检查运行状态异常:{}", e.getMessage());
data.put("error", e.getMessage());
}
result.put(sshModel.getId(), data);
}
return JsonMessage.getString(200, "", result);
}
JSONObject result = new JSONObject();
for (SshModel sshModel : sshModels) {
List<NodeModel> nodeBySshId = nodeService.getNodeBySshId(sshModel.getId());
JSONObject data = new JSONObject();
NodeModel nodeModel = CollUtil.getFirst(nodeBySshId);
SshModel model = sshService.getByKey(sshModel.getId(), false);
try {
if (nodeModel == null) {
Integer pid = sshService.checkSshRunPid(model, Type.Agent.getTag());
data.put("pid", ObjectUtil.defaultIfNull(pid, 0));
data.put("ok", true);
} else {
data.put("nodeId", nodeModel.getId());
data.put("nodeName", nodeModel.getName());
}
//
String javaVersion = sshService.checkCommand(model, "java");
data.put("javaVersion", javaVersion);
} catch (Exception e) {
DefaultSystemLog.getLog().error("检查运行状态异常:{}", e.getMessage());
data.put("error", e.getMessage());
}
result.put(sshModel.getId(), data);
}
return JsonMessage.getString(200, "", result);
}
@PostMapping(value = "del.json", produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(method = MethodFeature.DEL)
public String del(@ValidatorItem(value = ValidatorRule.NOT_BLANK) String id) {
HttpServletRequest request = getRequest();
boolean checkSsh = buildInfoService.checkReleaseMethodByLike(id, request, BuildReleaseMethod.Ssh);
Assert.state(!checkSsh, "当前ssh存在构建项不能删除");
// 判断是否绑定节点
List<NodeModel> nodeBySshId = nodeService.getNodeBySshId(id);
Assert.state(CollUtil.isEmpty(nodeBySshId), "当前ssh被节点绑定不能删除");
@PostMapping(value = "del.json", produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(method = MethodFeature.DEL)
public String del(@ValidatorItem(value = ValidatorRule.NOT_BLANK) String id) {
HttpServletRequest request = getRequest();
boolean checkSsh = buildInfoService.checkReleaseMethodByLike(id, request, BuildReleaseMethod.Ssh);
Assert.state(!checkSsh, "当前ssh存在构建项不能删除");
// 判断是否绑定节点
List<NodeModel> nodeBySshId = nodeService.getNodeBySshId(id);
Assert.state(CollUtil.isEmpty(nodeBySshId), "当前ssh被节点绑定不能删除");
sshService.delByKey(id, request);
//
int logCount = sshTerminalExecuteLogService.delByWorkspace(request, entity -> entity.set("sshId", id));
return JsonMessage.getString(200, "操作成功");
}
sshService.delByKey(id, request);
//
int logCount = sshTerminalExecuteLogService.delByWorkspace(request, entity -> entity.set("sshId", id));
return JsonMessage.getString(200, "操作成功");
}
/**
* 执行记录
*
* @return json
*/
@PostMapping(value = "log_list_data.json", produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(cls = ClassFeature.SSH_TERMINAL_LOG, method = MethodFeature.LIST)
public String logListData() {
PageResultDto<SshTerminalExecuteLog> pageResult = sshTerminalExecuteLogService.listPage(getRequest());
return JsonMessage.getString(200, "获取成功", pageResult);
}
/**
* 执行记录
*
* @return json
*/
@PostMapping(value = "log_list_data.json", produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(cls = ClassFeature.SSH_TERMINAL_LOG, method = MethodFeature.LIST)
public String logListData() {
PageResultDto<SshTerminalExecuteLog> pageResult = sshTerminalExecuteLogService.listPage(getRequest());
return JsonMessage.getString(200, "获取成功", pageResult);
}
/**
* 同步到指定工作空间
*
* @param ids 节点ID
* @param workspaceId 分配到到工作空间ID
* @return msg
*/
@GetMapping(value = "sync-to-workspace", produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(method = MethodFeature.EDIT)
@SystemPermission()
public String syncToWorkspace(@ValidatorItem String ids, @ValidatorItem String workspaceId) {
String nowWorkspaceId = nodeService.getCheckUserWorkspace(getRequest());
//
sshService.checkUserWorkspace(workspaceId);
sshService.syncToWorkspace(ids, nowWorkspaceId, workspaceId);
return JsonMessage.getString(200, "操作成功");
}
}

View File

@ -99,6 +99,11 @@ public class NodeModel extends BaseGroupModel {
this.setId(id);
}
public NodeModel(String id, String workspaceId) {
this.setId(id);
this.setWorkspaceId(workspaceId);
}
/**
* 获取 授权的信息
*

View File

@ -31,6 +31,8 @@ import io.jpom.model.BaseWorkspaceModel;
import io.jpom.service.h2db.TableName;
import io.jpom.util.StringUtil;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.nio.charset.Charset;
import java.util.List;
@ -43,6 +45,8 @@ import java.util.List;
*/
@TableName(value = "SSH_INFO", name = "SSH 信息")
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
public class SshModel extends BaseWorkspaceModel {
private String name;
@ -77,6 +81,14 @@ public class SshModel extends BaseWorkspaceModel {
*/
private String allowEditSuffix;
public SshModel(String id) {
this.setId(id);
}
public SshModel(String id, String workspaceId) {
this.setId(id);
this.setWorkspaceId(workspaceId);
}
public ConnectType connectType() {
return EnumUtil.fromString(ConnectType.class, this.connectType, ConnectType.PASS);

View File

@ -247,6 +247,8 @@ public abstract class BaseDbCommonService<T> {
* 根据主键查询实体
*
* @param keyValue 主键值
* @param fill 是否执行填充逻辑
* @param consumer 参数回调
* @return 数据
*/
public T getByKey(String keyValue, boolean fill, Consumer<Entity> consumer) {

View File

@ -53,6 +53,8 @@ import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
@ -242,6 +244,49 @@ public class NodeService extends BaseGroupService<NodeModel> {
super.update(nodeModel1);
}
/**
* 将节点信息同步到其他工作空间
*
* @param ids 多给节点ID
* @param nowWorkspaceId 当前的工作空间ID
* @param workspaceId 同步到哪个工作空间
*/
public void syncToWorkspace(String ids, String nowWorkspaceId, String workspaceId) {
StrUtil.splitTrim(ids, StrUtil.COMMA)
.forEach(id -> {
NodeModel data = super.getByKey(id, false, entity -> entity.set("workspaceId", nowWorkspaceId));
Assert.notNull(data, "没有对应到节点信息");
//
NodeModel where = new NodeModel();
where.setWorkspaceId(workspaceId);
where.setUrl(data.getUrl());
NodeModel nodeModel = NodeService.super.queryByBean(where);
if (nodeModel == null) {
// 不存在则添加节点
data.setId(null);
data.setWorkspaceId(workspaceId);
data.setCreateTimeMillis(null);
data.setModifyTimeMillis(null);
data.setModifyUser(null);
NodeService.super.insert(data);
} else {
// 修改信息
NodeModel update = new NodeModel(nodeModel.getId());
update.setLoginName(data.getLoginName());
update.setLoginPwd(data.getLoginPwd());
update.setProtocol(data.getProtocol());
update.setHttpProxy(data.getHttpProxy());
update.setHttpProxyType(data.getHttpProxyType());
NodeService.super.updateById(update);
}
});
}
/**
* 判断节点是否锁定中
*
* @param nodeModel 节点
*/
private void checkLockType(NodeModel nodeModel) {
if (nodeModel == null) {
return;
@ -304,19 +349,19 @@ public class NodeService extends BaseGroupService<NodeModel> {
* @param info 节点信息
*/
private void updateDuplicateNode(NodeModel info) {
if (StrUtil.hasEmpty(info.getUrl(), info.getLoginName(), info.getLoginPwd())) {
return;
}
NodeModel update = new NodeModel();
update.setLoginName(info.getLoginName());
update.setLoginPwd(info.getLoginPwd());
//
NodeModel where = new NodeModel();
where.setUrl(info.getUrl());
int updateCount = super.update(super.dataBeanToEntity(update), super.dataBeanToEntity(where));
if (updateCount > 1) {
DefaultSystemLog.getLog().debug("update duplicate node {} {}", info.getUrl(), updateCount);
}
// if (StrUtil.hasEmpty(info.getUrl(), info.getLoginName(), info.getLoginPwd())) {
// return;
// }
// NodeModel update = new NodeModel();
// update.setLoginName(info.getLoginName());
// update.setLoginPwd(info.getLoginPwd());
// //
// NodeModel where = new NodeModel();
// where.setUrl(info.getUrl());
// int updateCount = super.update(super.dataBeanToEntity(update), super.dataBeanToEntity(where));
// if (updateCount > 1) {
// DefaultSystemLog.getLog().debug("update duplicate node {} {}", info.getUrl(), updateCount);
// }
}
/**

View File

@ -37,8 +37,10 @@ import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpException;
import io.jpom.model.data.NodeModel;
import io.jpom.model.data.SshModel;
import io.jpom.service.h2db.BaseWorkspaceService;
import io.jpom.service.node.NodeService;
import io.jpom.system.ConfigBean;
import io.jpom.util.JschUtils;
import org.springframework.stereotype.Service;
@ -99,10 +101,10 @@ public class SshService extends BaseWorkspaceService<SshModel> {
} else if (StrUtil.startWith(privateKey, JschUtils.HEADER)) {
// 直接采用 private key content 登录无需写入文件
session = JschUtils.createSession(sshModel.getHost(),
sshModel.getPort(),
sshModel.getUser(),
StrUtil.trim(privateKey),
sshModel.password());
sshModel.getPort(),
sshModel.getUser(),
StrUtil.trim(privateKey),
sshModel.password());
} else if (StrUtil.isEmpty(privateKey)) {
File home = FileUtil.getUserHomeDir();
Assert.notNull(home, "用户目录没有找到");
@ -125,7 +127,7 @@ public class SshService extends BaseWorkspaceService<SshModel> {
// 简要私钥文件是否存在
Assert.state(FileUtil.isFile(rsaFile), "私钥文件不存在:" + FileUtil.getAbsolutePath(rsaFile));
session = JschUtil.openSession(sshModel.getHost(),
sshModel.getPort(), sshModel.getUser(), FileUtil.getAbsolutePath(rsaFile), sshModel.password());
sshModel.getPort(), sshModel.getUser(), FileUtil.getAbsolutePath(rsaFile), sshModel.password());
}
} else {
throw new IllegalArgumentException("不支持的模式");
@ -375,4 +377,42 @@ public class SshService extends BaseWorkspaceService<SshModel> {
JschUtil.close(session);
}
}
/**
* 将节点信息同步到其他工作空间
*
* @param ids 多给节点ID
* @param nowWorkspaceId 当前的工作空间ID
* @param workspaceId 同步到哪个工作空间
*/
public void syncToWorkspace(String ids, String nowWorkspaceId, String workspaceId) {
StrUtil.splitTrim(ids, StrUtil.COMMA)
.forEach(id -> {
SshModel data = super.getByKey(id, false, entity -> entity.set("workspaceId", nowWorkspaceId));
Assert.notNull(data, "没有对应到节点信息");
//
SshModel where = new SshModel();
where.setWorkspaceId(workspaceId);
where.setHost(data.getHost());
where.setPort(data.getPort());
where.setConnectType(data.getConnectType());
SshModel sshModel = super.queryByBean(where);
if (sshModel == null) {
// 不存在则添加 信息
data.setId(null);
data.setWorkspaceId(workspaceId);
data.setCreateTimeMillis(null);
data.setModifyTimeMillis(null);
data.setModifyUser(null);
super.insert(data);
} else {
// 修改信息
SshModel update = new SshModel(data.getId());
update.setUser(data.getUser());
update.setPassword(data.getPassword());
update.setPrivateKey(data.getPrivateKey());
super.updateById(update);
}
});
}
}

View File

@ -96,6 +96,14 @@ export function unLockWorkspace(params) {
});
}
export function syncToWorkspace(params) {
return axios({
url: "/node/sync-to-workspace",
method: "get",
params: params,
});
}
// 删除节点项目缓存
export function delAllProjectCache() {
return axios({

View File

@ -238,3 +238,11 @@ export function newFileFolder(params) {
data: params,
});
}
export function syncToWorkspace(params) {
return axios({
url: "/node/ssh/sync-to-workspace",
method: "get",
params: params,
});
}

View File

@ -11,6 +11,7 @@
@expand="expand"
:pagination="this.listQuery.total / this.listQuery.limit > 1 ? (this, pagination) : false"
@change="changePage"
:row-selection="rowSelection"
>
<template slot="title">
<a-space>
@ -30,20 +31,20 @@
<a-menu-item>
<a-button type="primary" @click="fastInstall">快速安装</a-button>
</a-menu-item>
<a-menu-item>
<a-button type="primary" :disabled="!tableSelections || !tableSelections.length" @click="syncToWorkspaceShow">工作空间同步</a-button>
</a-menu-item>
</a-menu>
</a-dropdown>
<a-tooltip>
<template slot="title">
<div>点击节点管理进入节点管理页面</div>
<div>
<ul>
<li>节点账号密码为插件端的账号密码,并非用户账号(管理员)密码</li>
<li>节点账号密码默认由系统生成可以通过插件端数据目录下 agent_authorize.json 文件查看如果自定义配置了账号密码将没有此文件</li>
<li>节点地址为插件端的 IP:PORT 插件端端口默认为2123</li>
</ul>
</div>
<ul>
<li>点击节点管理进入节点管理页面</li>
<li>节点账号密码为插件端的账号密码,并非用户账号(管理员)密码</li>
<li>节点账号密码默认由系统生成可以通过插件端数据目录下 agent_authorize.json 文件查看如果自定义配置了账号密码将没有此文件</li>
<li>节点地址为插件端的 IP:PORT 插件端端口默认为2123</li>
</ul>
</template>
<a-icon type="question-circle" theme="filled" />
</a-tooltip>
@ -384,11 +385,44 @@
</div>
<div v-else>loading....</div>
</a-modal>
<!-- 同步到其他工作空间 -->
<a-modal v-model="syncToWorkspaceVisible" title="同步到其他工作空间" @ok="handleSyncToWorkspace" :maskClosable="false">
<a-alert message="温馨提示" type="warning">
<template slot="description">
<ul>
<li>同步机制采用节点地址确定为是同一个服务器节点</li>
<li>当目标工作空间不存在对应的节点时候将自动创建一个新的节点逻辑节点</li>
<li>当目标工作空间已经存在节点时候将自动同步节点授权信息代理配置信息</li>
</ul>
</template>
</a-alert>
<a-form-model :model="temp" :label-col="{ span: 6 }" :wrapper-col="{ span: 14 }">
<a-form-model-item> </a-form-model-item>
<a-form-model-item label="选择工作空间" prop="workspaceId">
<a-select show-search option-filter-prop="children" v-model="temp.workspaceId" placeholder="请选择工作空间">
<a-select-option :disabled="getWorkspaceId === item.id" v-for="item in workspaceList" :key="item.id">{{ item.name }}</a-select-option>
</a-select>
</a-form-model-item>
</a-form-model>
</a-modal>
</div>
</template>
<script>
import { mapGetters } from "vuex";
import { getNodeList, getNodeStatus, editNode, unbind, deleteNode, syncProject, unLockWorkspace, getNodeGroupAll, fastInstall, pullFastInstallResult, confirmFastInstall } from "@/api/node";
import {
getNodeList,
getNodeStatus,
editNode,
unbind,
deleteNode,
syncProject,
unLockWorkspace,
getNodeGroupAll,
fastInstall,
pullFastInstallResult,
confirmFastInstall,
syncToWorkspace,
} from "@/api/node";
import { getSshListAll } from "@/api/ssh";
import { syncScript } from "@/api/node-other";
import NodeLayout from "./node-layout";
@ -416,9 +450,7 @@ export default {
nodeStatusData: {},
groupList: [],
showOptVisible: {},
temp: {
timeOut: 0,
},
temp: {},
fastInstallActiveKey: ["1", "2", "4"],
fastInstallInfo: null,
tempVue: null,
@ -429,6 +461,7 @@ export default {
terminalVisible: false,
unlockNode: false,
fastInstallNode: false,
syncToWorkspaceVisible: false,
drawerTitle: "",
columns: [
// { title: " ID", dataIndex: "id", sorter: true, key: "id", ellipsis: true, scopedSlots: { customRender: "id" } },
@ -473,6 +506,7 @@ export default {
timeOut: [{ required: true, message: "Please input timeout", trigger: "blur" }],
},
workspaceList: [],
tableSelections: [],
};
},
computed: {
@ -490,6 +524,14 @@ export default {
},
};
},
rowSelection() {
return {
onChange: (selectedRowKeys) => {
this.tableSelections = selectedRowKeys;
},
selectedRowKeys: this.tableSelections,
};
},
},
watch: {
$route() {
@ -789,7 +831,7 @@ export default {
okText: "确认",
cancelText: "取消",
onOk: () => {
//
//
unLockWorkspace({
id: this.temp.id,
workspaceId: this.temp.workspaceId,
@ -862,6 +904,37 @@ export default {
}
});
},
//
syncToWorkspaceShow() {
this.syncToWorkspaceVisible = true;
this.loadWorkSpaceListAll();
this.temp = {
workspaceId: undefined,
};
},
//
handleSyncToWorkspace() {
if (!this.temp.workspaceId) {
this.$notification.warn({
message: "请选择工作空间",
});
return false;
}
//
syncToWorkspace({
ids: this.tableSelections.join(","),
workspaceId: this.temp.workspaceId,
}).then((res) => {
if (res.code == 200) {
this.$notification.success({
message: res.msg,
});
this.tableSelections = [];
this.syncToWorkspaceVisible = false;
return false;
}
});
},
},
};
</script>

View File

@ -83,10 +83,10 @@
<a-button size="small" type="primary" @click="handleEdit(record)">编辑</a-button>
</a-menu-item>
<a-menu-item>
<a-button size="small" type="primary" @click="handleMonitor(record)" v-show="javaModes.includes(record.runMode)" :disabled="!record.status">监控 </a-button>
<a-button size="small" type="primary" @click="handleMonitor(record)" v-if="javaModes.includes(record.runMode)" :disabled="!record.status">监控 </a-button>
</a-menu-item>
<a-menu-item>
<a-button size="small" type="primary" @click="handleReplica(record)" v-show="javaModes.includes(record.runMode)" :disabled="!record.javaCopyItemList">副本集 </a-button>
<a-button size="small" type="primary" @click="handleReplica(record)" v-if="javaModes.includes(record.runMode)" :disabled="!record.javaCopyItemList">副本集 </a-button>
</a-menu-item>
<a-menu-item>
<a-tooltip v-if="record.outGivingProject" title="节点分发项目需要到节点分发中去删除">

View File

@ -11,7 +11,8 @@
:pagination="this.listQuery.total / this.listQuery.limit > 1 ? (this, pagination) : false"
@change="changePage"
bordered
:rowKey="(record, index) => index"
rowKey="id"
:row-selection="rowSelection"
>
<template slot="title">
<a-space>
@ -22,7 +23,14 @@
<a-button type="primary" :loading="loading" @click="loadData">搜索</a-button>
</a-tooltip>
<a-button type="primary" @click="handleAdd">新增</a-button>
<a-dropdown>
<a class="ant-dropdown-link" @click="(e) => e.preventDefault()"> 更多 <a-icon type="down" /> </a>
<a-menu slot="overlay">
<a-menu-item>
<a-button type="primary" :disabled="!tableSelections || !tableSelections.length" @click="syncToWorkspaceShow">工作空间同步</a-button>
</a-menu-item>
</a-menu>
</a-dropdown>
<a-tooltip>
<template slot="title">
<div>
@ -58,7 +66,12 @@
<a-button size="small" type="primary" @click="install(record)" :disabled="sshAgentInfo[record.id].pid > 0">安装节点</a-button>
</a-tooltip>
</div>
<div v-else><a-tag>没有Java环境</a-tag></div>
<div v-else>
<a-tooltip v-if="sshAgentInfo[record.id].error" :title="sshAgentInfo[record.id].error">
<a-tag>连接异常</a-tag>
</a-tooltip>
<a-tag v-else>没有Java环境</a-tag>
</div>
</div>
<div v-else>-</div>
</template>
@ -309,15 +322,37 @@
</template>
</a-table>
</a-modal>
<!-- 同步到其他工作空间 -->
<a-modal v-model="syncToWorkspaceVisible" title="同步到其他工作空间" @ok="handleSyncToWorkspace" :maskClosable="false">
<a-alert message="温馨提示" type="warning">
<template slot="description">
<ul>
<li>同步机制采用 IP+PORT+连接方式 确定为是同一个服务器</li>
<li>当目标工作空间不存在对应的 SSH 时候将自动创建一个新的 SSH</li>
<li>当目标工作空间已经存在 SSH 时候将自动同步 SSH 账号密码私钥信息</li>
</ul>
</template>
</a-alert>
<a-form-model :model="temp" :label-col="{ span: 6 }" :wrapper-col="{ span: 14 }">
<a-form-model-item> </a-form-model-item>
<a-form-model-item label="选择工作空间" prop="workspaceId">
<a-select show-search option-filter-prop="children" v-model="temp.workspaceId" placeholder="请选择工作空间">
<a-select-option :disabled="getWorkspaceId === item.id" v-for="item in workspaceList" :key="item.id">{{ item.name }}</a-select-option>
</a-select>
</a-form-model-item>
</a-form-model>
</a-modal>
</div>
</template>
<script>
import { deleteSsh, editSsh, getSshList, getSshCheckAgent, getSshOperationLogList, installAgentNode, getAgent, uploadAgent } from "@/api/ssh";
import { deleteSsh, editSsh, getSshList, getSshCheckAgent, getSshOperationLogList, installAgentNode, getAgent, uploadAgent, syncToWorkspace } from "@/api/ssh";
import SshFile from "@/pages/ssh/ssh-file";
import Terminal from "@/pages/ssh/terminal";
import { parseTime } from "@/utils/time";
import { PAGE_DEFAULT_LIMIT, PAGE_DEFAULT_SIZW_OPTIONS, PAGE_DEFAULT_SHOW_TOTAL, PAGE_DEFAULT_LIST_QUERY } from "@/utils/const";
import { getWorkSpaceListAll } from "@/api/workspace";
import Vue from "vue";
import { mapGetters } from "vuex";
export default {
components: {
@ -332,6 +367,9 @@ export default {
listQuery: Object.assign({}, PAGE_DEFAULT_LIST_QUERY),
editSshVisible: false,
nodeVisible: false,
syncToWorkspaceVisible: false,
tableSelections: [],
workspaceList: [],
tempNode: {},
fileList: [],
sshAgentInfo: {},
@ -397,7 +435,7 @@ export default {
dataIndex: "nodeId",
scopedSlots: { customRender: "nodeId" },
width: 120,
// ellipsis: true,
ellipsis: true,
},
{
title: "修改时间",
@ -446,6 +484,7 @@ export default {
};
},
computed: {
...mapGetters(["getWorkspaceId"]),
viewOperationLogPagination() {
return {
total: this.viewOperationLogListQuery.total || 0,
@ -472,6 +511,14 @@ export default {
},
};
},
rowSelection() {
return {
onChange: (selectedRowKeys) => {
this.tableSelections = selectedRowKeys;
},
selectedRowKeys: this.tableSelections,
};
},
},
created() {
this.loadData();
@ -731,6 +778,45 @@ export default {
onClose() {
this.drawerVisible = false;
},
//
loadWorkSpaceListAll() {
getWorkSpaceListAll().then((res) => {
if (res.code === 200) {
this.workspaceList = res.data;
}
});
},
//
syncToWorkspaceShow() {
this.syncToWorkspaceVisible = true;
this.loadWorkSpaceListAll();
this.temp = {
workspaceId: undefined,
};
},
//
handleSyncToWorkspace() {
if (!this.temp.workspaceId) {
this.$notification.warn({
message: "请选择工作空间",
});
return false;
}
//
syncToWorkspace({
ids: this.tableSelections.join(","),
workspaceId: this.temp.workspaceId,
}).then((res) => {
if (res.code == 200) {
this.$notification.success({
message: res.msg,
});
this.tableSelections = [];
this.syncToWorkspaceVisible = false;
return false;
}
});
},
},
};
</script>