feat(image): 添加虚拟机克隆功能并优化相关操作

- 新增 ExternalTorrentClient 接口,用于调用第三方 API
- 在 ImageVirtualMachinesServiceImpl 中实现虚拟机克隆功能
- 优化虚拟机启动、关闭、销毁和重启操作
- 在 application.yml 中添加外部 API 客户端配置
- 更新 ExternalApiClient 接口,增加获取虚拟机信息的方法
master
chenhao 2025-09-02 20:56:01 +08:00
parent a421493709
commit f7a5e8c69c
6 changed files with 97 additions and 31 deletions

View File

@ -128,6 +128,8 @@ public class ImageVirtualMachinesReq {
@JsonProperty("memory_total") @JsonProperty("memory_total")
@ApiModelProperty("内存大小(MB)") @ApiModelProperty("内存大小(MB)")
private Integer memoryTotal; private Integer memoryTotal;
@JsonProperty("auto_mount_virtio")
private Boolean autoMountVirtio;
/** /**
* (GB) * (GB)
**/ **/

View File

@ -5,6 +5,7 @@ import com.unisinsight.project.entity.dto.ApiResponse;
import com.unisinsight.project.entity.dto.Network; import com.unisinsight.project.entity.dto.Network;
import com.unisinsight.project.entity.dto.NetworkData; import com.unisinsight.project.entity.dto.NetworkData;
import com.unisinsight.project.entity.dto.StoragePoolData; import com.unisinsight.project.entity.dto.StoragePoolData;
import com.unisinsight.project.entity.dto.VmInfoDTO;
import com.unisinsight.project.entity.req.*; import com.unisinsight.project.entity.req.*;
import com.unisinsight.project.entity.res.ImageStatusRes; import com.unisinsight.project.entity.res.ImageStatusRes;
import org.springframework.cloud.openfeign.FeignClient; import org.springframework.cloud.openfeign.FeignClient;
@ -75,6 +76,9 @@ public interface ExternalApiClient {
@PostMapping("/api/v1/vm/start") @PostMapping("/api/v1/vm/start")
ApiResponse<ImageStatusRes> startImage(@RequestBody ImageOperationReq operationReq); ApiResponse<ImageStatusRes> startImage(@RequestBody ImageOperationReq operationReq);
@GetMapping("/api/v1/vm/{vm_name}")
ApiResponse<VmInfoDTO> getVmInfo(@PathVariable("vm_name") String vmName);
@PostMapping("/api/v1/vm/shutdown") @PostMapping("/api/v1/vm/shutdown")
ApiResponse<ImageStatusRes> shutdownImage(@RequestBody ImageOperationReq operationReq); ApiResponse<ImageStatusRes> shutdownImage(@RequestBody ImageOperationReq operationReq);

View File

@ -0,0 +1,42 @@
package com.unisinsight.project.feign;
/**
* @Author : ch
* @version : 1.0
* @ClassName : ExternalTorrentClient
* @Description :
* @DATE : Created in 17:12 2025/9/2
* <pre> Copyright: Copyright(c) 2025 </pre>
* <pre> Company : </pre>
* Modification History:
* Date Author Version Discription
* --------------------------------------------------------------------------
* 2025/09/02 ch 1.0 Why & What is modified: <> *
*/
import com.unisinsight.project.config.FeignConfig;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* API
* @author ch
*/
@FeignClient(
name = "external-torrent-client",
url = "${external.torrent.url:http://localhost:8114}",
configuration = FeignConfig.class
)
public interface ExternalTorrentClient {
@GetMapping("/vdi/desk-image")
boolean start(@RequestParam("srcFile") String srcFile,
@RequestParam("detFile") String detFile,
@RequestParam("name") String name,
@RequestParam("type") String type);
@GetMapping("/vdi/progress")
Double progress(@RequestParam("name") String name);
}

View File

@ -6,6 +6,7 @@ import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.unisinsight.project.entity.dao.Image;
import com.unisinsight.project.entity.dao.ImageTool; import com.unisinsight.project.entity.dao.ImageTool;
import com.unisinsight.project.entity.req.DeleteIdReq; import com.unisinsight.project.entity.req.DeleteIdReq;
import com.unisinsight.project.entity.req.ImageToolReq; import com.unisinsight.project.entity.req.ImageToolReq;
@ -20,6 +21,7 @@ import com.unisinsight.project.service.ChunkedUploadService;
import com.unisinsight.project.service.ImageToolService; import com.unisinsight.project.service.ImageToolService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -56,6 +58,8 @@ public class ImageToolServiceImpl extends ServiceImpl<ImageToolMapper, ImageTool
public Result<PageResult<ImageToolRes>> selectPage(ImageToolReq imageToolReq) { public Result<PageResult<ImageToolRes>> selectPage(ImageToolReq imageToolReq) {
Page<ImageTool> page = new Page<>(imageToolReq.getPageNum(), imageToolReq.getPageSize()); Page<ImageTool> page = new Page<>(imageToolReq.getPageNum(), imageToolReq.getPageSize());
LambdaQueryWrapper<ImageTool> queryWrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<ImageTool> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(StringUtils.isNotBlank(imageToolReq.getFileName()), ImageTool::getFileName, imageToolReq.getFileName());
queryWrapper.eq(StringUtils.isNotBlank(imageToolReq.getFileType()), ImageTool::getFileType, imageToolReq.getFileType());
queryWrapper.orderByAsc(ImageTool::getId); queryWrapper.orderByAsc(ImageTool::getId);
Page<ImageTool> imageToolPage = mapper.selectPage(page, queryWrapper); Page<ImageTool> imageToolPage = mapper.selectPage(page, queryWrapper);
log.info("分页查询返回:{}", JSONUtil.toJsonStr(imageToolPage)); log.info("分页查询返回:{}", JSONUtil.toJsonStr(imageToolPage));

View File

@ -9,25 +9,20 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.unisinsight.project.entity.dao.Image; import com.unisinsight.project.entity.dao.Image;
import com.unisinsight.project.entity.dao.ImageDesktop;
import com.unisinsight.project.entity.dao.ImageTool; import com.unisinsight.project.entity.dao.ImageTool;
import com.unisinsight.project.entity.dao.ImageVirtualMachines; import com.unisinsight.project.entity.dao.ImageVirtualMachines;
import com.unisinsight.project.entity.dao.ImageDesktop;
import com.unisinsight.project.entity.dto.ApiResponse; import com.unisinsight.project.entity.dto.ApiResponse;
import com.unisinsight.project.entity.req.DeleteIdReq; import com.unisinsight.project.entity.dto.VmInfoDTO;
import com.unisinsight.project.entity.req.ImageCloneToDesktopReq; import com.unisinsight.project.entity.req.*;
import com.unisinsight.project.entity.req.ImageCreateReq;
import com.unisinsight.project.entity.req.ImageDeleteReq;
import com.unisinsight.project.entity.req.ImageOperationReq;
import com.unisinsight.project.entity.req.ImageUpdateReq;
import com.unisinsight.project.entity.req.ImageVirtualMachinesReq;
import com.unisinsight.project.entity.res.ImageStatusRes; import com.unisinsight.project.entity.res.ImageStatusRes;
import com.unisinsight.project.entity.res.ImageVirtualMachinesRes; import com.unisinsight.project.entity.res.ImageVirtualMachinesRes;
import com.unisinsight.project.entity.res.PageResult; import com.unisinsight.project.entity.res.PageResult;
import com.unisinsight.project.exception.BaseErrorCode; import com.unisinsight.project.exception.BaseErrorCode;
import com.unisinsight.project.exception.Result; import com.unisinsight.project.exception.Result;
import com.unisinsight.project.feign.ExternalApiClient; import com.unisinsight.project.feign.ExternalApiClient;
import com.unisinsight.project.feign.ExternalTorrentClient;
import com.unisinsight.project.mapper.ImageVirtualMachinesMapper; import com.unisinsight.project.mapper.ImageVirtualMachinesMapper;
import com.unisinsight.project.service.ImageDesktopService; import com.unisinsight.project.service.ImageDesktopService;
import com.unisinsight.project.service.ImageService; import com.unisinsight.project.service.ImageService;
import com.unisinsight.project.service.ImageToolService; import com.unisinsight.project.service.ImageToolService;
@ -36,7 +31,6 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.Resource; import javax.annotation.Resource;
@ -59,6 +53,8 @@ public class ImageVirtualMachinesServiceImpl extends ServiceImpl<ImageVirtualMac
private ImageService imageService; private ImageService imageService;
@Autowired @Autowired
private ExternalApiClient externalApiClient; private ExternalApiClient externalApiClient;
@Autowired
private ExternalTorrentClient externalTorrentClient;
@Resource @Resource
private ImageToolService imageToolService; private ImageToolService imageToolService;
@Autowired @Autowired
@ -161,7 +157,7 @@ public class ImageVirtualMachinesServiceImpl extends ServiceImpl<ImageVirtualMac
.storagePoolName(imageVirtualMachinesReq.getStoragePoolName()) .storagePoolName(imageVirtualMachinesReq.getStoragePoolName())
.networkName(imageVirtualMachinesReq.getNetworkModule()) .networkName(imageVirtualMachinesReq.getNetworkModule())
.isoPath(systemImage.getStoragePath()) .isoPath(systemImage.getStoragePath())
.autoMountVirtio(false) .autoMountVirtio(imageVirtualMachinesReq.getAutoMountVirtio())
.autostart(false) .autostart(false)
//驱动 //驱动
.virtioWinPath(imageTool.getStorePath()) .virtioWinPath(imageTool.getStorePath())
@ -190,7 +186,7 @@ public class ImageVirtualMachinesServiceImpl extends ServiceImpl<ImageVirtualMac
public Result<?> update(ImageVirtualMachinesReq imageVirtualMachinesReq) { public Result<?> update(ImageVirtualMachinesReq imageVirtualMachinesReq) {
//查询驱动信息 //查询驱动信息
LambdaQueryWrapper<ImageTool> imageToolLambdaQueryWrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<ImageTool> imageToolLambdaQueryWrapper = new LambdaQueryWrapper<>();
imageToolLambdaQueryWrapper.eq(ImageTool::getFileName, imageVirtualMachinesReq.getImageName()); imageToolLambdaQueryWrapper.eq(ImageTool::getFileName, imageVirtualMachinesReq.getImageToolName());
ImageTool imageTool = imageToolService.getOne(imageToolLambdaQueryWrapper); ImageTool imageTool = imageToolService.getOne(imageToolLambdaQueryWrapper);
// 调用镜像修改服务 // 调用镜像修改服务
ImageUpdateReq updateReq = ImageUpdateReq.builder() ImageUpdateReq updateReq = ImageUpdateReq.builder()
@ -265,24 +261,33 @@ public class ImageVirtualMachinesServiceImpl extends ServiceImpl<ImageVirtualMac
@Override @Override
public Result<?> cloneTemplate(ImageVirtualMachinesReq req) { public Result<?> cloneTemplate(ImageVirtualMachinesReq req) {
// todo 改为调用脚本方式执行
// 查询虚拟机信息 // 查询虚拟机信息
ImageVirtualMachines imageVirtualMachines = machinesMapper.selectById(req.getId()); LambdaQueryWrapper<ImageVirtualMachines> queryWrapper=new LambdaQueryWrapper<>();
queryWrapper.eq(ImageVirtualMachines::getImageName, req.getImageName());
ImageVirtualMachines imageVirtualMachines = getOne(queryWrapper);
if (ObjectUtils.isEmpty(imageVirtualMachines)) { if (ObjectUtils.isEmpty(imageVirtualMachines)) {
log.info("查询镜像虚拟机返回为空"); log.info("查询镜像虚拟机返回为空");
return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500, "虚拟机不存在"); return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500, "虚拟机不存在");
} }
// 调用远程接口获取虚拟机详细信息
ApiResponse<VmInfoDTO> vmInfoResponse = externalApiClient.getVmInfo(req.getImageName());
if (vmInfoResponse == null || !"200".equals(vmInfoResponse.getCode()) || vmInfoResponse.getData() == null) {
log.error("获取虚拟机信息失败: {}", vmInfoResponse);
return Result.errorResult(BaseErrorCode.HTTP_REQUEST_FAILURE, "获取虚拟机信息失败");
}
// 从返回数据中提取需要的信息
VmInfoDTO vmInfo = vmInfoResponse.getData();
// 调用克隆虚拟机到桌面镜像服务 String diskPath = vmInfo.getDiskPath();
ImageCloneToDesktopReq cloneReq = ImageCloneToDesktopReq.builder()
.vmName(imageVirtualMachines.getImageName())
.desktopName(req.getImageName() + "_desktop") // 使用虚拟机名称加desktop作为桌面镜像名称
.storagePath("/vms/iso") // 默认存储路径
.description("从虚拟机克隆的桌面镜像")
.build();
ApiResponse<ImageStatusRes> response = externalApiClient.cloneTemplate(cloneReq); // 根据虚拟机信息调用远程虚拟机信息
if (response == null || !"200".equals(response.getCode())) { boolean response = externalTorrentClient.start(diskPath,
diskPath.replaceAll(imageVirtualMachines.getImageName(),imageVirtualMachines.getImageName()+"_desktop"),
imageVirtualMachines.getImageName()+".json", "vhd");
if (!response) {
return Result.errorResult(BaseErrorCode.HTTP_REQUEST_FAILURE, "克隆虚拟机到桌面镜像失败"); return Result.errorResult(BaseErrorCode.HTTP_REQUEST_FAILURE, "克隆虚拟机到桌面镜像失败");
} }
@ -291,9 +296,6 @@ public class ImageVirtualMachinesServiceImpl extends ServiceImpl<ImageVirtualMac
imageDesktop.setDesktopName(req.getImageName() + "_desktop"); imageDesktop.setDesktopName(req.getImageName() + "_desktop");
imageDesktop.setImageVirtualId(Math.toIntExact(req.getId())); imageDesktop.setImageVirtualId(Math.toIntExact(req.getId()));
imageDesktop.setOsVersion(imageVirtualMachines.getOsVersion()); imageDesktop.setOsVersion(imageVirtualMachines.getOsVersion());
// imageDesktop.setStoragePath("/vms/iso/" + req.getImageName() + "_desktop.qcow2"); // 假设是qcow2格式
// imageDesktop.setDesktopType(3); // QCOW2格式
// imageDesktop.setPublishStatus("unpublished"); // 默认未发布状态
imageDesktop.setCreateUser("admin"); imageDesktop.setCreateUser("admin");
imageDesktop.setUpdateUser("admin"); imageDesktop.setUpdateUser("admin");
imageDesktop.setDescription("从虚拟机克隆的桌面镜像"); imageDesktop.setDescription("从虚拟机克隆的桌面镜像");
@ -311,7 +313,9 @@ public class ImageVirtualMachinesServiceImpl extends ServiceImpl<ImageVirtualMac
@Override @Override
public Result<?> start(ImageVirtualMachinesReq req) { public Result<?> start(ImageVirtualMachinesReq req) {
// 查询虚拟机信息 // 查询虚拟机信息
ImageVirtualMachines imageVirtualMachines = machinesMapper.selectById(req.getId()); LambdaQueryWrapper<ImageVirtualMachines> queryWrapper=new LambdaQueryWrapper<>();
queryWrapper.eq(ImageVirtualMachines::getImageName, req.getImageName());
ImageVirtualMachines imageVirtualMachines = getOne(queryWrapper);
if (ObjectUtils.isEmpty(imageVirtualMachines)) { if (ObjectUtils.isEmpty(imageVirtualMachines)) {
log.info("查询镜像虚拟机返回为空"); log.info("查询镜像虚拟机返回为空");
return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500, "虚拟机不存在"); return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500, "虚拟机不存在");
@ -332,7 +336,9 @@ public class ImageVirtualMachinesServiceImpl extends ServiceImpl<ImageVirtualMac
@Override @Override
public Result<?> shutdown(ImageVirtualMachinesReq req) { public Result<?> shutdown(ImageVirtualMachinesReq req) {
// 查询虚拟机信息 // 查询虚拟机信息
ImageVirtualMachines imageVirtualMachines = machinesMapper.selectById(req.getId()); LambdaQueryWrapper<ImageVirtualMachines> queryWrapper=new LambdaQueryWrapper<>();
queryWrapper.eq(ImageVirtualMachines::getImageName, req.getImageName());
ImageVirtualMachines imageVirtualMachines = getOne(queryWrapper);
if (ObjectUtils.isEmpty(imageVirtualMachines)) { if (ObjectUtils.isEmpty(imageVirtualMachines)) {
log.info("查询镜像虚拟机返回为空"); log.info("查询镜像虚拟机返回为空");
return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500, "虚拟机不存在"); return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500, "虚拟机不存在");
@ -353,7 +359,9 @@ public class ImageVirtualMachinesServiceImpl extends ServiceImpl<ImageVirtualMac
@Override @Override
public Result<?> destroy(ImageVirtualMachinesReq req) { public Result<?> destroy(ImageVirtualMachinesReq req) {
// 查询虚拟机信息 // 查询虚拟机信息
ImageVirtualMachines imageVirtualMachines = machinesMapper.selectById(req.getId()); LambdaQueryWrapper<ImageVirtualMachines> queryWrapper=new LambdaQueryWrapper<>();
queryWrapper.eq(ImageVirtualMachines::getImageName, req.getImageName());
ImageVirtualMachines imageVirtualMachines = getOne(queryWrapper);
if (ObjectUtils.isEmpty(imageVirtualMachines)) { if (ObjectUtils.isEmpty(imageVirtualMachines)) {
log.info("查询镜像虚拟机返回为空"); log.info("查询镜像虚拟机返回为空");
return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500, "虚拟机不存在"); return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500, "虚拟机不存在");
@ -374,7 +382,9 @@ public class ImageVirtualMachinesServiceImpl extends ServiceImpl<ImageVirtualMac
@Override @Override
public Result<?> reboot(ImageVirtualMachinesReq req) { public Result<?> reboot(ImageVirtualMachinesReq req) {
// 查询虚拟机信息 // 查询虚拟机信息
ImageVirtualMachines imageVirtualMachines = machinesMapper.selectById(req.getId()); LambdaQueryWrapper<ImageVirtualMachines> queryWrapper=new LambdaQueryWrapper<>();
queryWrapper.eq(ImageVirtualMachines::getImageName, req.getImageName());
ImageVirtualMachines imageVirtualMachines = getOne(queryWrapper);
if (ObjectUtils.isEmpty(imageVirtualMachines)) { if (ObjectUtils.isEmpty(imageVirtualMachines)) {
log.info("查询镜像虚拟机返回为空"); log.info("查询镜像虚拟机返回为空");
return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500, "虚拟机不存在"); return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500, "虚拟机不存在");

View File

@ -58,6 +58,8 @@ feign:
config: config:
external-api-client: # 对应 FeignClient 的 name external-api-client: # 对应 FeignClient 的 name
logger-level: full # 完整日志级别 logger-level: full # 完整日志级别
external-torrent-client:
logger-level: full
default: # 全局默认配置 default: # 全局默认配置
logger-level: basic logger-level: basic
@ -68,4 +70,6 @@ logging:
com.unisinsight.project.feign.ExternalApiClient: debug com.unisinsight.project.feign.ExternalApiClient: debug
external: external:
api: api:
url: http://10.100.51.178:8000 url: http://10.100.51.178:8000
torrent:
url: http://10.100.51.178:8114