package com.vci.ubcs.deploy.service.impl; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.alibaba.nacos.shaded.com.google.protobuf.ServiceException; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; import com.vci.ubcs.deploy.entity.DeployApps; import com.vci.ubcs.deploy.enumpack.CmdConfigEnum; import com.vci.ubcs.deploy.mapper.DeployAppsMapper; import com.vci.ubcs.deploy.service.IDeployAppsService; import com.vci.ubcs.deploy.vo.DeployAppsVO; import com.vci.ubcs.starter.util.HttpUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springblade.core.tool.api.R; import org.springblade.core.tool.utils.Func; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.context.EnvironmentAware; import org.springframework.core.env.Environment; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import java.io.*; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; /** * @author ludc * @date 2024/1/7 19:54 */ @Service @RequiredArgsConstructor @Slf4j public class DeployAppsServiceImpl extends ServiceImpl implements IDeployAppsService, EnvironmentAware { private final DeployAppsMapper deployAppsMapper; @Value("${password-free.pwd-free-addr:localhost}") private String pwdFreeAddr; /** * 通过服务注册中心获取网关的端口号 */ @Autowired private DiscoveryClient discoveryClient; /** * 各个服务存放的的父路径 */ @Value("${local-log.parent-path:/data1/ubcs/ubcs-server}") private String PARENTPATH; /** * 日志文件的具体位置 */ @Value("${local-log.log-path:/target/log}") private String LOGPATH; /** * 根据操作系统生成分隔符 */ private String SEPARATOR = "/"; /** * 根据当前运行的环境,对配置的日志路径格式进行调整 * @param environment */ @Override public void setEnvironment(Environment environment) { String os = environment.getProperty("os.name").toLowerCase(); if (!os.contains("win")) { this.SEPARATOR = "/"; }else{ this.SEPARATOR = "\\"; } } @Override public List getApplications(ServletRequest servletRequest) throws ServiceException { HttpServletRequest request = (HttpServletRequest) servletRequest; String baldeAuth = request.getHeaders("Blade-Auth").nextElement(); String cookie = request.getHeaders("Cookie").nextElement(); // 免密登录接口地址 String loginUrl = "http://"+pwdFreeAddr+":"+this.getGatewayPort("ubcs-gateway")+"/ubcs-admin/applications"; log.debug("获取服务信息调用地址:"+loginUrl); // 请求ubcs-admin获取服务信息 HttpHeaders headers = new HttpHeaders(); ArrayList mediaTypes = new ArrayList<>(); mediaTypes.add(MediaType.APPLICATION_JSON); headers.set("Authorization","Basic c2FiZXI6c2FiZXJfc2VjcmV0"); headers.set("Blade-Auth",baldeAuth); headers.set("Cookie",cookie); headers.setAccept(mediaTypes); List deployAppsVOList = new ArrayList<>(); try { // 发送GET请求 String responseBody = HttpUtils.get(loginUrl, headers); if(Func.isNotEmpty(responseBody)){ // 将 JSON 字符串转换为 JSONArray JSONArray jsonArray = JSON.parseArray(responseBody); for (Object obj : jsonArray) { DeployAppsVO deployAppsVO = new DeployAppsVO(); JSONObject jsonObject = (JSONObject) obj; List instances = ((List) jsonObject.get("instances")); deployAppsVO.setName(jsonObject.get("name").toString()); String serviceUrl = ((JSONObject) ((JSONObject) instances.get(0)).get("registration")).get("serviceUrl").toString(); deployAppsVO.setPort(String.valueOf(new URL(serviceUrl).getPort())); deployAppsVO.setServiceNum(instances.size()); deployAppsVO.setStatus(jsonObject.get("status").toString()); deployAppsVO.setStatusTimestamp(jsonObject.get("statusTimestamp").toString()); deployAppsVOList.add(deployAppsVO); } } }catch (Exception e){ throw new ServiceException("调用ubcs-admin获取服务信息失败,原因:"+e.getMessage()); } // 再查询库中已经存在的服务配置信息,进行是否已经启动的判断 List deployApps = deployAppsMapper.selectList(null); // 库中未配置,直接返回正在运行的服务信息 if(deployApps.isEmpty()){ return deployAppsVOList; } // 筛选出不在运行的并生成默认的服务信息 List deployAppsVOS1 = deployApps.stream() .filter(deployApp -> deployAppsVOList.stream() .noneMatch(deployAppVO -> deployApp.getServerName().equals(deployAppVO.getName()) && deployApp.getServerName().equals(deployAppVO.getName()))) .map(deployApp -> { DeployAppsVO deployAppsVO2 = new DeployAppsVO(deployApp.getServerName(), "DOWN", "", "", 0); if(deployApp.getServerName().equals("web")){ deployAppsVO2.setStatus("UP"); deployAppsVO2.setPort("8080"); deployAppsVO2.setServiceNum(1); } return deployAppsVO2; }) .collect(Collectors.toList()); deployAppsVOList.addAll(deployAppsVOS1); return deployAppsVOList; } @Override public DeployApps saveOrGetServiceConfInfo(DeployAppsVO deployAppsVO) throws ServiceException { if(deployAppsVO.getName().isEmpty()){ throw new ServiceException("缺少必传参数name"); } List deployApps = deployAppsMapper.selectList(Wrappers.query().lambda().eq(DeployApps::getServerName, deployAppsVO.getName())); if(Func.isNotEmpty(deployApps)){ return deployApps.get(0); } // 未从库中查询到,需要生成服务信息保存默认信息到库中 DeployApps defaultDeployApps = new DeployApps(); defaultDeployApps.setLogPath(PARENTPATH + SEPARATOR + deployAppsVO.getName().replace("-","_") + LOGPATH); defaultDeployApps.setServerName(deployAppsVO.getName()); defaultDeployApps.setStartCmd(CmdConfigEnum.START_CMD.getValue() + deployAppsVO.getName()); defaultDeployApps.setStopCmd(CmdConfigEnum.STOP_CMD.getValue() + deployAppsVO.getName()); defaultDeployApps.setRestartCmd(CmdConfigEnum.RESTART_CMD.getValue() + deployAppsVO.getName()); defaultDeployApps.setServerPath(PARENTPATH + SEPARATOR + deployAppsVO.getName().replace("-","_")); int eft = deployAppsMapper.insert(defaultDeployApps); if (!SqlHelper.retBool(eft)) { throw new ServiceException("生成默认服务信息到库中时失败!"); } return defaultDeployApps; } /** * 修改或保存 * @param deployAppsVO * @return * @throws ServiceException */ @Override public R saveOrUpdateServiceInfo(DeployAppsVO deployAppsVO) throws ServiceException { boolean checkBoolean = checkCmdVer(deployAppsVO.getStartCmd()) && checkCmdVer(deployAppsVO.getStopCmd()) && checkCmdVer(deployAppsVO.getRestartCmd()); if(checkBoolean){ return R.fail("配置的命令中包含危险命令,如:rm、mv、rm -rf、chmod等命令和关键字! "); } return R.status(this.saveOrUpdate(deployAppsVO)); } /** * 新增服务信息 * @param deployApps * @return * @throws ServiceException */ @Override public boolean addSave(DeployApps deployApps) throws ServiceException { if (Func.isBlank(deployApps.getServerName()) || Func.isBlank(deployApps.getServerPath())) { throw new ServiceException("必传参数[服务名称,服务存放路径]不能为空"); } List deployAppsList = deployAppsMapper.selectList(Wrappers.query().lambda().eq(DeployApps::getServerName, deployApps.getServerName())); if (!deployAppsList.isEmpty()) { throw new ServiceException("新增服务的服务名重复!"); } return SqlHelper.retBool(deployAppsMapper.insert(deployApps)); } /** * 更新文件上传 * @param files * @param serverName * @return */ @Override public R importJarUpdate(MultipartFile[] files, String serverName) throws ServiceException { // 根据服务名查看到服务相关信息 List deployAppsDB = deployAppsMapper.selectList(Wrappers.query().lambda().eq(DeployApps::getServerName, serverName)); if(deployAppsDB.isEmpty()){ return R.fail("No configuration information related to "+ serverName +" service found"); } // 遍历MultipartFile数组,逐个处理文件 try { for (MultipartFile file : files) { // 配置了备份文件路径,先备份再替换 if(Func.isNotEmpty(deployAppsDB.get(0).getFileBack())){ File backFile = new File(deployAppsDB.get(0).getFileBack()); // 路径不存在就创建 if (!backFile.exists()) { backFile.mkdirs(); } String backName = ""; String fileType = "file"; // 是压缩文件,因为只会存在两种情况,文件名是压缩文件,或者文件(.jar类型的文件) // 当前时间 String currentDateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); if (file.getContentType().equals("application/zip") || file.getContentType().equals("application/x-zip-compressed")) { backName = file.getOriginalFilename().replace(".zip","_" + currentDateTime); }else{ backName = file.getOriginalFilename().replace(".","_" + currentDateTime + "."); } File source = new File(deployAppsDB.get(0).getServerPath() + this.SEPARATOR + file.getOriginalFilename().replace(".zip", "")); File destination = new File(deployAppsDB.get(0).getFileBack() + this.SEPARATOR + backName); copyFolder(source, destination); } Path filePath = Paths.get(deployAppsDB.get(0).getServerPath(), file.getOriginalFilename()); Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING); // 检查文件类型,如果是压缩文件则解压缩 if (file.getContentType().equals("application/zip") || file.getContentType().equals("application/x-zip-compressed")) { //sourcePath压缩包文件路径 try (ZipFile zipFile = new ZipFile(new File(deployAppsDB.get(0).getServerPath()+ this.SEPARATOR +file.getOriginalFilename()))) { Enumeration enumeration = zipFile.entries(); while (enumeration.hasMoreElements()) { //依次获取压缩包内的文件实体对象 ZipEntry entry = (ZipEntry) enumeration.nextElement(); String name = entry.getName(); if (entry.isDirectory()) { continue; } try (BufferedInputStream inputStream = new BufferedInputStream(zipFile.getInputStream(entry))) { // 需要判断文件所在的目录是否存在,处理压缩包里面有文件夹的情况 String outName = deployAppsDB.get(0).getServerPath() + this.SEPARATOR + name; File outFile = new File(outName); File tempFile = new File(outName.substring(0, outName.lastIndexOf("/"))); if (!tempFile.exists()) { tempFile.mkdirs(); } try (BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(outFile))) { int len; byte[] buffer = new byte[1024]; while ((len = inputStream.read(buffer)) > 0) { outputStream.write(buffer, 0, len); } } } } } catch (Exception e) { e.printStackTrace(); } File file1 = new File(deployAppsDB.get(0).getServerPath() + SEPARATOR + file.getOriginalFilename()); // 压缩文件上传成功之后,删除解压文件 file1.delete(); } } String output = execute(deployAppsDB.get(0),"UP"); return R.success(output.toString()); } catch (IOException e) { e.printStackTrace(); log.error(e.getMessage()); return R.fail("Failed to upload files"); } } /** * 执行命令 * @param deployAppsVO * @return */ @Override public R cmdExecute(DeployAppsVO deployAppsVO) throws ServiceException { String excuteRes = ""; try { List deployAppsDB = deployAppsMapper.selectList(Wrappers.query().lambda().eq(DeployApps::getServerName, deployAppsVO.getName())); if(deployAppsDB.isEmpty()){ return R.fail("命令执行出错,库中未找到"+ deployAppsVO.getName() +"服务相关配置:" ); } excuteRes = execute(deployAppsDB.get(0),deployAppsVO.getStatus()); return R.success("命令执行结束:"+excuteRes); }catch (Exception e){ throw new ServiceException(e.getMessage()); } } /** * 执行命令 * @param deployApps * @return * @throws ServiceException */ public String execute(DeployApps deployApps,String type) throws ServiceException { // 处理上传文件的逻辑 StringBuilder output = new StringBuilder(); try { String cmd = ""; if(type.equalsIgnoreCase("UP")){ cmd = deployApps.getRestartCmd(); }else { cmd = deployApps.getStartCmd(); } if(Func.isEmpty(cmd)){ return "The executed command is empty"; } if(checkCmdVer(cmd)){ return "配置的命令中包含危险命令,如:rm、mv、rm -rf、chmod等命令和关键词! "; } // 执行Linux命令 log.info("开始执行命令:"+cmd); // Process process = Runtime.getRuntime().exec(cmd); ProcessBuilder processBuilder = new ProcessBuilder(cmd.split("\\s")); // processBuilder.command(cmd); Process process = processBuilder.start(); // 等待命令执行完成 int exitCode = process.waitFor(); InputStream inputStream = process.getInputStream(); OutputStream outputStream = process.getOutputStream(); InputStream errorStream = process.getErrorStream(); // 读取命令执行结果 BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); String line; while ((line = reader.readLine()) != null) { output.append(line).append("\n"); } reader.close(); // 读取命令执行的错误输出流 BufferedReader errorReader = new BufferedReader(new InputStreamReader(errorStream)); StringBuilder errorOutput = new StringBuilder(); String errorLine; while ((errorLine = errorReader.readLine()) != null) { errorOutput.append(errorLine).append("\n"); } String errorOutputString = errorOutput.toString(); errorReader.close(); log.info("错误输出:" + errorOutputString); int exitValue = process.exitValue(); log.info("命令执行结果:" + output.toString()+":"+exitCode+","+exitValue); return output.toString(); }catch (IOException | InterruptedException e){ e.printStackTrace(); log.error("命令执行出错,原因:" + e.getMessage()); throw new ServiceException("Command execution failed"+e.getMessage()); } } /** * 文件备份操作 * @param source 源文件 * @param destination 文件备份路径 * @throws IOException */ private void copyFolder(File source, File destination) throws IOException { // 文件存在才需要备份 if(source.exists()){ if (source.isDirectory()) { if (!destination.exists()) { destination.mkdir(); } String[] files = source.list(); for (String file : files) { File srcFile = new File(source, file); File destFile = new File(destination, file); copyFolder(srcFile, destFile); } } else { Files.copy(source.toPath(), destination.toPath()); } } } /** * 根据服务名获取端口号 * @param serviceId * @return */ private String getGatewayPort(String serviceId) { List instances = discoveryClient.getInstances(serviceId); if (!instances.isEmpty()) { ServiceInstance gatewayInstance = instances.get(0); return String.valueOf(gatewayInstance.getPort()); } return "8080"; } /** * 启动、停止、重启命令校验 * @param cmd * @return true: 包含高风险命令, false:不包含 */ private boolean checkCmdVer(String cmd){ if(Func.isEmpty(cmd)){ return false; } String[] highRiskCommands = {"rm", "rmdir", "mv", "unlink", "rm -rf", "mv -rf", "dd", "chmod", "chown", "mkfs", "shutdown", "reboot", "kill"}; for(String highRiskCmd : highRiskCommands){ if(cmd.contains(highRiskCmd)){ return true; } } return false; } }