From 0292dcb065f6152a870b7d8155558d962a343bad Mon Sep 17 00:00:00 2001 From: chenhao <852066789@qq.com> Date: Tue, 2 Sep 2025 09:56:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(virtual-machines):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=99=9A=E6=8B=9F=E6=9C=BA=E6=93=8D=E4=BD=9C=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E5=92=8C=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增了虚拟机启动、关闭、强制关闭、重启等操作的接口和实现 - 增加了克隆虚拟机到桌面镜像的功能 - 更新了虚拟机删除逻辑,支持删除存储的镜像文件 - 重构了部分代码,优化了虚拟机相关数据的处理 --- .../controller/ImageToolController.java | 50 ++++- .../ImageVirtualMachinesController.java | 31 +-- .../properties/ImageConfigProperties.java | 38 ++++ .../project/service/ImageToolService.java | 6 +- .../service/ImageVirtualMachinesService.java | 30 +++ .../service/impl/ClientServiceImpl.java | 14 +- .../service/impl/ImageToolServiceImpl.java | 7 +- .../impl/ImageVirtualMachinesServiceImpl.java | 176 +++++++++++++++++- nex-be/src/main/resources/application.yml | 6 + 9 files changed, 332 insertions(+), 26 deletions(-) diff --git a/nex-be/src/main/java/com/unisinsight/project/controller/ImageToolController.java b/nex-be/src/main/java/com/unisinsight/project/controller/ImageToolController.java index f1d38a4..b589b95 100644 --- a/nex-be/src/main/java/com/unisinsight/project/controller/ImageToolController.java +++ b/nex-be/src/main/java/com/unisinsight/project/controller/ImageToolController.java @@ -11,11 +11,17 @@ import com.unisinsight.project.service.ImageToolService; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; -import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.io.File; import java.io.Serializable; import java.util.List; @@ -43,7 +49,7 @@ public class ImageToolController { /** * 服务对象 */ - @Resource + @javax.annotation.Resource private ImageToolService service; @Autowired private ChunkedUploadService chunkedUploadService; @@ -90,12 +96,12 @@ public class ImageToolController { @RequestParam("shard_total") int totalChunks, @RequestParam("file_name") String fileName, @RequestParam("file_size") long totalSize, - @RequestParam("file_version") String fileVersion, + @RequestParam(value = "file_version",required = false) String fileVersion, @RequestParam(value = "description", required = false) String description ) { return service.uploadChunk(chunk, chunkSize, chunkMd5, fileId, chunkNumber, totalChunks, - fileName, totalSize,description); + fileName, totalSize,description,fileType,fileVersion); } @PostMapping("/cancel/upload") @ApiOperation(value = "取消上传ImageTool文件") @@ -108,6 +114,42 @@ public class ImageToolController { return Result.errorResultMessage(BaseErrorCode.HTTP_ERROR_CODE_500, "取消上传失败: " + e.getMessage()); } } + + @GetMapping("/download/{id}") + @ApiOperation(value = "下载ImageTool文件", notes = "通过文件ID下载对应的ImageTool文件") + public ResponseEntity downloadFile(@PathVariable Integer id, HttpServletRequest request) { + try { + // 1. 查询文件信息 + ImageTool imageTool = service.getById(id); + if (imageTool == null || imageTool.getStorePath() == null) { + return ResponseEntity.notFound().build(); + } + + // 2. 获取文件路径 + String filePath = imageTool.getStorePath(); + File file = new File(filePath); + + if (!file.exists()) { + return ResponseEntity.notFound().build(); + } + + // 3. 构建响应 + FileSystemResource resource = new FileSystemResource(file); + String contentType = request.getServletContext().getMimeType(file.getAbsolutePath()); + if(contentType == null) { + contentType = "application/octet-stream"; + } + + return ResponseEntity.ok() + .contentType(MediaType.parseMediaType(contentType)) + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + imageTool.getFileName() + "\"") + .body(resource); + } catch (Exception e) { + log.error("下载文件失败,id:{},error:{}", id, e.getMessage(), e); + return ResponseEntity.internalServerError().build(); + } + } + @ApiOperation(value = "修改") @PostMapping("/update") public Result updateUser(@RequestBody ImageToolReq imageToolReq) { diff --git a/nex-be/src/main/java/com/unisinsight/project/controller/ImageVirtualMachinesController.java b/nex-be/src/main/java/com/unisinsight/project/controller/ImageVirtualMachinesController.java index 5bba7fc..f2bc3d2 100644 --- a/nex-be/src/main/java/com/unisinsight/project/controller/ImageVirtualMachinesController.java +++ b/nex-be/src/main/java/com/unisinsight/project/controller/ImageVirtualMachinesController.java @@ -106,8 +106,26 @@ public class ImageVirtualMachinesController { return Result.errorResult(BaseErrorCode.PARAMS_CHK_ERROR); } log.info("终端启动请求参数为:{}", JSONUtil.toJsonStr(req)); - return service.cloneTemplate(req); + return service.start(req); } + @ApiOperation(value = "终端关闭") + @PostMapping("/shutdown") + public Result shutdown(@RequestBody ImageVirtualMachinesReq req) { + if (Objects.isNull(req)) { + return Result.errorResult(BaseErrorCode.PARAMS_CHK_ERROR); + } + log.info("终端关闭请求参数为:{}", JSONUtil.toJsonStr(req)); + return service.shutdown(req); + } +// @ApiOperation(value = "终端强制关闭") +// @PostMapping("/destroy") +// public Result destroy(@RequestBody ImageVirtualMachinesReq req) { +// if (Objects.isNull(req)) { +// return Result.errorResult(BaseErrorCode.PARAMS_CHK_ERROR); +// } +// log.info("终端强制关闭请求参数为:{}", JSONUtil.toJsonStr(req)); +// return service.destroy(req); +// } @ApiOperation(value = "终端重启") @PostMapping("/reboot") public Result reboot(@RequestBody ImageVirtualMachinesReq req) { @@ -115,16 +133,7 @@ public class ImageVirtualMachinesController { return Result.errorResult(BaseErrorCode.PARAMS_CHK_ERROR); } log.info("终端重启请求参数为:{}", JSONUtil.toJsonStr(req)); - return service.cloneTemplate(req); - } - @ApiOperation(value = "终端快照") - @PostMapping("/snapshot") - public Result snapshot(@RequestBody ImageVirtualMachinesReq req) { - if (Objects.isNull(req)) { - return Result.errorResult(BaseErrorCode.PARAMS_CHK_ERROR); - } - log.info("终端快照请求参数为:{}", JSONUtil.toJsonStr(req)); - return service.cloneTemplate(req); + return service.reboot(req); } } diff --git a/nex-be/src/main/java/com/unisinsight/project/properties/ImageConfigProperties.java b/nex-be/src/main/java/com/unisinsight/project/properties/ImageConfigProperties.java index 5b5434b..9329491 100644 --- a/nex-be/src/main/java/com/unisinsight/project/properties/ImageConfigProperties.java +++ b/nex-be/src/main/java/com/unisinsight/project/properties/ImageConfigProperties.java @@ -43,6 +43,19 @@ public class ImageConfigProperties { private String updateUrl; @Getter(value=AccessLevel.NONE) private String startUrl; + @Getter(value=AccessLevel.NONE) + private String shutdownUrl; + @Getter(value=AccessLevel.NONE) + private String destroyUrl; + @Getter(value=AccessLevel.NONE) + private String rebootUrl; + @Getter(value=AccessLevel.NONE) + private String pauseUrl; + @Getter(value=AccessLevel.NONE) + private String resumeUrl; + @Getter(value=AccessLevel.NONE) + private String cloneToDesktopUrl; + @@ -65,4 +78,29 @@ public class ImageConfigProperties { public String getStartUrl() { return baseUrl+startUrl; } + + public String getShutdownUrl() { + return baseUrl+shutdownUrl; + } + + public String getDestroyUrl() { + return baseUrl+destroyUrl; + } + + public String getRebootUrl() { + return baseUrl+rebootUrl; + } + + public String getPauseUrl() { + return baseUrl+pauseUrl; + } + + public String getResumeUrl() { + return baseUrl+resumeUrl; + } + + public String getCloneToDesktopUrl() { + return baseUrl+cloneToDesktopUrl; + } + } diff --git a/nex-be/src/main/java/com/unisinsight/project/service/ImageToolService.java b/nex-be/src/main/java/com/unisinsight/project/service/ImageToolService.java index 3285d37..e5d74de 100644 --- a/nex-be/src/main/java/com/unisinsight/project/service/ImageToolService.java +++ b/nex-be/src/main/java/com/unisinsight/project/service/ImageToolService.java @@ -27,6 +27,10 @@ public interface ImageToolService extends IService { Result delete(DeleteIdReq deleteIdReq); - Result uploadChunk(MultipartFile chunk, int chunkSize, String chunkMd5, String fileId, int chunkNumber, int totalChunks, String fileName, long totalSize, String description); + Result uploadChunk(MultipartFile chunk, int chunkSize, String chunkMd5, + String fileId, int chunkNumber, + int totalChunks, String fileName, + long totalSize, String description + , String fileType,String fileVersion); } diff --git a/nex-be/src/main/java/com/unisinsight/project/service/ImageVirtualMachinesService.java b/nex-be/src/main/java/com/unisinsight/project/service/ImageVirtualMachinesService.java index a501160..4d9da9a 100644 --- a/nex-be/src/main/java/com/unisinsight/project/service/ImageVirtualMachinesService.java +++ b/nex-be/src/main/java/com/unisinsight/project/service/ImageVirtualMachinesService.java @@ -27,5 +27,35 @@ public interface ImageVirtualMachinesService extends IService delete(DeleteIdReq deleteIdReq); Result cloneTemplate(ImageVirtualMachinesReq req); + + /** + * 启动虚拟机 + * @param req + * @return + */ + Result start(ImageVirtualMachinesReq req); + + /** + * 关闭虚拟机 + * @param req + * @return + */ + Result shutdown(ImageVirtualMachinesReq req); + + /** + * 强制关闭虚拟机 + * @param req + * @return + */ + Result destroy(ImageVirtualMachinesReq req); + + /** + * 重启虚拟机 + * @param req + * @return + */ + Result reboot(ImageVirtualMachinesReq req); + + } diff --git a/nex-be/src/main/java/com/unisinsight/project/service/impl/ClientServiceImpl.java b/nex-be/src/main/java/com/unisinsight/project/service/impl/ClientServiceImpl.java index dff5e09..aa49f75 100644 --- a/nex-be/src/main/java/com/unisinsight/project/service/impl/ClientServiceImpl.java +++ b/nex-be/src/main/java/com/unisinsight/project/service/impl/ClientServiceImpl.java @@ -6,9 +6,12 @@ import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.unisinsight.project.entity.dao.DeviceImageMapping; import com.unisinsight.project.entity.dao.Image; +import com.unisinsight.project.entity.dao.ImageDesktop; +import com.unisinsight.project.entity.res.ImageDesktopRes; import com.unisinsight.project.entity.res.ImageRes; import com.unisinsight.project.exception.Result; import com.unisinsight.project.mapper.DeviceImageMappingMapper; +import com.unisinsight.project.mapper.ImageDesktopMapper; import com.unisinsight.project.mapper.ImageMapper; import com.unisinsight.project.service.ClientService; import lombok.extern.slf4j.Slf4j; @@ -33,7 +36,7 @@ import java.util.stream.Collectors; public class ClientServiceImpl implements ClientService { @Resource - private ImageMapper imageMapper; + private ImageDesktopMapper imageDesktopMapper; @Resource private DeviceImageMappingMapper deviceImageMappingMapper; @@ -44,6 +47,7 @@ public class ClientServiceImpl implements ClientService { @Override public Result getImageList(String deviceId, String token) { +// todo 改为桌面镜像 需验证 HashMap hashMap = new HashMap<>(); List deviceImageMappings = deviceImageMappingMapper.selectList(new LambdaQueryWrapper().eq(DeviceImageMapping::getDeviceId, deviceId)); if (CollectionUtil.isEmpty(deviceImageMappings)) { @@ -52,13 +56,13 @@ public class ClientServiceImpl implements ClientService { } List imageIdList = deviceImageMappings.stream().map(DeviceImageMapping::getImageId).filter(Objects::nonNull).distinct().collect(Collectors.toList()); if (CollectionUtil.isNotEmpty(imageIdList)) { - List images = imageMapper.selectList(new LambdaQueryWrapper().in(Image::getId, imageIdList)); + List images = imageDesktopMapper.selectList(new LambdaQueryWrapper().in(ImageDesktop::getId, imageIdList)); log.info("用户登录查询镜像结果:{}", JSONUtil.toJsonStr(images)); - List imageRes = BeanUtil.copyToList(images, ImageRes.class); + List imageRes = BeanUtil.copyToList(images, ImageDesktopRes.class); List> collect = imageRes.stream().distinct().map(e -> { HashMap map = new HashMap<>(); - if (StringUtils.isNotBlank(e.getImageFileName())) { - map.put("name", e.getImageFileName()); + if (StringUtils.isNotBlank(e.getDesktopName())) { + map.put("name", e.getDesktopName()); } if (StringUtils.isNotBlank(e.getBtPath())) { if (e.getBtPath().contains("http://") || e.getBtPath().contains("https://")) { diff --git a/nex-be/src/main/java/com/unisinsight/project/service/impl/ImageToolServiceImpl.java b/nex-be/src/main/java/com/unisinsight/project/service/impl/ImageToolServiceImpl.java index 6470b17..9ab0ec0 100644 --- a/nex-be/src/main/java/com/unisinsight/project/service/impl/ImageToolServiceImpl.java +++ b/nex-be/src/main/java/com/unisinsight/project/service/impl/ImageToolServiceImpl.java @@ -123,7 +123,9 @@ public class ImageToolServiceImpl extends ServiceImpl uploadChunk(MultipartFile chunk, int chunkSize, String chunkMd5, String fileId, int chunkNumber, int totalChunks, String fileName, long totalSize, String description) { + public Result uploadChunk(MultipartFile chunk, int chunkSize, String chunkMd5, String fileId, int chunkNumber, + int totalChunks, String fileName, long totalSize, String description + , String fileType, String fileVersion) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(ImageTool::getFileName, fileName); ImageTool exists = mapper.selectOne(queryWrapper); @@ -142,7 +144,8 @@ public class ImageToolServiceImpl extends ServiceImpl delete(DeleteIdReq deleteIdReq) { + // 先查询要删除的虚拟机信息 + ImageVirtualMachines imageVirtualMachines = machinesMapper.selectById(deleteIdReq.getId()); + if (ObjectUtils.isEmpty(imageVirtualMachines)) { + log.info("查询镜像虚拟机返回为空"); + return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500, "虚拟机不存在"); + } + + // 调用镜像删除服务 + ImageDeleteReq deleteReq = ImageDeleteReq.builder() + .vmName(imageVirtualMachines.getImageName()) + .deleteStorage(true) + .build(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity requestEntity = new HttpEntity<>(deleteReq, headers); + ResponseEntity exchange = restTemplate.exchange(imageConfigProperties.getDeleteUrl(), HttpMethod.POST, requestEntity, ImageStatusRes.class); + if (!exchange.getStatusCode().equals(HttpStatus.OK)) { + return Result.errorResult(BaseErrorCode.HTTP_REQUEST_FAILURE, "删除虚拟机失败"); + } + + // 删除数据库记录 int deleted = machinesMapper.deleteById(deleteIdReq.getId()); log.info("终端删除delete:{}", deleted); if (deleted == 1) { @@ -243,8 +271,150 @@ public class ImageVirtualMachinesServiceImpl extends ServiceImpl cloneTemplate(ImageVirtualMachinesReq req) { - //todo 调用镜像生成服务 在生成完毕后生成桌面镜像数据 桌面镜像加BT - return null; + // 查询虚拟机信息 + ImageVirtualMachines imageVirtualMachines = machinesMapper.selectById(req.getId()); + if (ObjectUtils.isEmpty(imageVirtualMachines)) { + log.info("查询镜像虚拟机返回为空"); + return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500, "虚拟机不存在"); + } + + // 调用克隆虚拟机到桌面镜像服务 + ImageCloneToDesktopReq cloneReq = ImageCloneToDesktopReq.builder() + .vmName(imageVirtualMachines.getImageName()) + .desktopName(req.getImageName() + "_desktop") // 使用虚拟机名称加desktop作为桌面镜像名称 + .storagePath("/vms/iso") // 默认存储路径 + .description("从虚拟机克隆的桌面镜像") + .build(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity requestEntity = new HttpEntity<>(cloneReq, headers); + String cloneToDesktopUrl = imageConfigProperties.getCloneToDesktopUrl(); + ResponseEntity exchange = restTemplate.exchange(cloneToDesktopUrl, HttpMethod.POST, requestEntity, ImageStatusRes.class); + if (!exchange.getStatusCode().equals(HttpStatus.OK)) { + return Result.errorResult(BaseErrorCode.HTTP_REQUEST_FAILURE, "克隆虚拟机到桌面镜像失败"); + } + + // 克隆成功后生成桌面镜像数据 + ImageDesktop imageDesktop = new ImageDesktop(); + imageDesktop.setDesktopName(req.getImageName() + "_desktop"); + imageDesktop.setImageVirtualId(Math.toIntExact(req.getId())); + imageDesktop.setOsVersion(imageVirtualMachines.getOsVersion()); +// imageDesktop.setStoragePath("/vms/iso/" + req.getImageName() + "_desktop.qcow2"); // 假设是qcow2格式 +// imageDesktop.setDesktopType(3); // QCOW2格式 +// imageDesktop.setPublishStatus("unpublished"); // 默认未发布状态 + imageDesktop.setCreateUser("admin"); + imageDesktop.setUpdateUser("admin"); + imageDesktop.setDescription("从虚拟机克隆的桌面镜像"); + + // 保存桌面镜像数据 + boolean saved = imageDesktopService.save(imageDesktop); + if (!saved) { + log.error("保存桌面镜像数据失败"); + return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500, "保存桌面镜像数据失败"); + } + + return Result.successResult(); } + + @Override + public Result start(ImageVirtualMachinesReq req) { + // 查询虚拟机信息 + ImageVirtualMachines imageVirtualMachines = machinesMapper.selectById(req.getId()); + if (ObjectUtils.isEmpty(imageVirtualMachines)) { + log.info("查询镜像虚拟机返回为空"); + return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500, "虚拟机不存在"); + } + + // 调用启动虚拟机服务 + ImageOperationReq operationReq = ImageOperationReq.builder() + .vmName(imageVirtualMachines.getImageName()) + .build(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity requestEntity = new HttpEntity<>(operationReq, headers); + ResponseEntity exchange = restTemplate.exchange(imageConfigProperties.getStartUrl(), HttpMethod.POST, requestEntity, ImageStatusRes.class); + if (!exchange.getStatusCode().equals(HttpStatus.OK)) { + return Result.errorResult(BaseErrorCode.HTTP_REQUEST_FAILURE, "启动虚拟机失败"); + } + + return Result.successResult(); + } + + @Override + public Result shutdown(ImageVirtualMachinesReq req) { + // 查询虚拟机信息 + ImageVirtualMachines imageVirtualMachines = machinesMapper.selectById(req.getId()); + if (ObjectUtils.isEmpty(imageVirtualMachines)) { + log.info("查询镜像虚拟机返回为空"); + return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500, "虚拟机不存在"); + } + + // 调用关闭虚拟机服务 + ImageOperationReq operationReq = ImageOperationReq.builder() + .vmName(imageVirtualMachines.getImageName()) + .build(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity requestEntity = new HttpEntity<>(operationReq, headers); + String shutdownUrl = imageConfigProperties.getShutdownUrl(); + ResponseEntity exchange = restTemplate.exchange(shutdownUrl, HttpMethod.POST, requestEntity, ImageStatusRes.class); + if (!exchange.getStatusCode().equals(HttpStatus.OK)) { + return Result.errorResult(BaseErrorCode.HTTP_REQUEST_FAILURE, "关闭虚拟机失败"); + } + + return Result.successResult(); + } + + @Override + public Result destroy(ImageVirtualMachinesReq req) { + // 查询虚拟机信息 + ImageVirtualMachines imageVirtualMachines = machinesMapper.selectById(req.getId()); + if (ObjectUtils.isEmpty(imageVirtualMachines)) { + log.info("查询镜像虚拟机返回为空"); + return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500, "虚拟机不存在"); + } + + // 调用强制关闭虚拟机服务 + ImageOperationReq operationReq = ImageOperationReq.builder() + .vmName(imageVirtualMachines.getImageName()) + .build(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity requestEntity = new HttpEntity<>(operationReq, headers); + String destroyUrl = imageConfigProperties.getDestroyUrl(); + ResponseEntity exchange = restTemplate.exchange(destroyUrl, HttpMethod.POST, requestEntity, ImageStatusRes.class); + if (!exchange.getStatusCode().equals(HttpStatus.OK)) { + return Result.errorResult(BaseErrorCode.HTTP_REQUEST_FAILURE, "强制关闭虚拟机失败"); + } + + return Result.successResult(); + } + + @Override + public Result reboot(ImageVirtualMachinesReq req) { + // 查询虚拟机信息 + ImageVirtualMachines imageVirtualMachines = machinesMapper.selectById(req.getId()); + if (ObjectUtils.isEmpty(imageVirtualMachines)) { + log.info("查询镜像虚拟机返回为空"); + return Result.errorResult(BaseErrorCode.HTTP_ERROR_CODE_500, "虚拟机不存在"); + } + + // 调用重启虚拟机服务 + ImageOperationReq operationReq = ImageOperationReq.builder() + .vmName(imageVirtualMachines.getImageName()) + .build(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity requestEntity = new HttpEntity<>(operationReq, headers); + String rebootUrl = imageConfigProperties.getRebootUrl(); + ResponseEntity exchange = restTemplate.exchange(rebootUrl, HttpMethod.POST, requestEntity, ImageStatusRes.class); + if (!exchange.getStatusCode().equals(HttpStatus.OK)) { + return Result.errorResult(BaseErrorCode.HTTP_REQUEST_FAILURE, "重启虚拟机失败"); + } + + return Result.successResult(); + } + } diff --git a/nex-be/src/main/resources/application.yml b/nex-be/src/main/resources/application.yml index 608d2dd..7f8db3e 100644 --- a/nex-be/src/main/resources/application.yml +++ b/nex-be/src/main/resources/application.yml @@ -15,6 +15,12 @@ image: delete-url: /api/v1/vm/delete update-url: /api/v1/vm/update start-url: /api/v1/vm/start + shutdown-url: /api/v1/vm/shutdown + destroy-url: /api/v1/vm/destroy + reboot-url: /api/v1/vm/reboot + pause-url: /api/v1/vm/pause + resume-url: /api/v1/vm/resume + clone-to-desktop-url: /api/v1/vm/clone-to-desktop spring: servlet: multipart: