Copilot commented on code in PR #12991:
URL: https://github.com/apache/cloudstack/pull/12991#discussion_r3063875262


##########
api/src/main/java/org/apache/cloudstack/api/command/user/volume/CreateVolumeCmd.java:
##########
@@ -153,6 +160,13 @@ private Long getProjectId() {
         return projectId;
     }
 
+    public Long getStorageId() {
+        if (snapshotId != null && storageId != null) {
+            throw new IllegalArgumentException("StorageId parameter cannot be 
specified with the SnapshotId parameter.");

Review Comment:
   Throwing `IllegalArgumentException` from a cmd accessor can result in 
inconsistent API error handling (often surfacing as 500). Prefer raising a 
`ServerApiException` with `ApiErrorCode.PARAM_ERROR` (or using built-in 
parameter validation) so callers receive a clear, standard client error 
response.
   ```suggestion
               throw new ServerApiException(ApiErrorCode.PARAM_ERROR,
                       "StorageId parameter cannot be specified with the 
SnapshotId parameter.");
   ```



##########
plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/DataCenterJoinVOToDataCenterConverter.java:
##########
@@ -0,0 +1,78 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.cloudstack.veeam.api.converter;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.cloudstack.veeam.VeeamControlService;
+import org.apache.cloudstack.veeam.api.DataCentersRouteHandler;
+import org.apache.cloudstack.veeam.api.dto.DataCenter;
+import org.apache.cloudstack.veeam.api.dto.Link;
+import org.apache.cloudstack.veeam.api.dto.Ref;
+import org.apache.cloudstack.veeam.api.dto.SupportedVersions;
+import org.apache.cloudstack.veeam.api.dto.Version;
+
+import com.cloud.api.query.vo.DataCenterJoinVO;
+import com.cloud.org.Grouping;
+
+public class DataCenterJoinVOToDataCenterConverter {
+    public static DataCenter toDataCenter(final DataCenterJoinVO zone) {
+        final String id = zone.getUuid();
+        final String basePath = VeeamControlService.ContextPath.value();
+        final String href = basePath + DataCentersRouteHandler.BASE_ROUTE + 
DataCentersRouteHandler.BASE_ROUTE + "/" + id;

Review Comment:
   The `href` concatenation duplicates `BASE_ROUTE`, producing an incorrect 
URL. Also, `Link.of(rel, href)` is being called with parameters reversed (rel 
is being set to a URL and href to a literal like `clusters`). Build `href` as 
`basePath + BASE_ROUTE + \"/\" + id`, and construct links as 
`Link.of(\"clusters\", href + \"/clusters\")` (same for 
networks/storagedomains).



##########
engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java:
##########
@@ -577,7 +576,13 @@ public void allocate(final String vmInstanceName, final 
VirtualMachineTemplate t
 
             logger.debug("Allocating disks for {}",  persistedVm);
 
-            allocateRootVolume(persistedVm, template, rootDiskOfferingInfo, 
owner, rootDiskSizeFinal, volume, snapshot);
+            if (_userVmMgr.isBlankInstance(template)) {
+                logger.debug("Template is a dummy template for hypervisor {}, 
skipping volume allocation", hyperType);
+                return;

Review Comment:
   Returning from `allocate(...)` when the template is a blank/dummy template 
likely aborts the rest of the VM allocation flow (beyond root volume 
allocation). If the intent is only to skip root volume allocation, avoid 
`return` here and instead conditionally skip `allocateRootVolume(...)` while 
letting the method continue.
   ```suggestion
                   logger.debug("Template is a dummy template for hypervisor 
{}, skipping root volume allocation", hyperType);
   ```



##########
core/src/main/java/org/apache/cloudstack/backup/CreateImageTransferCommand.java:
##########
@@ -0,0 +1,94 @@
+//Licensed to the Apache Software Foundation (ASF) under one
+//or more contributor license agreements.  See the NOTICE file
+//distributed with this work for additional information
+//regarding copyright ownership.  The ASF licenses this file
+//to you under the Apache License, Version 2.0 (the
+//"License"); you may not use this file except in compliance
+//the License.  You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing,
+//software distributed under the License is distributed on an
+//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+//KIND, either express or implied.  See the License for the
+//specific language governing permissions and limitations
+//under the License.
+
+package org.apache.cloudstack.backup;
+
+import com.cloud.agent.api.Command;
+
+public class CreateImageTransferCommand extends Command {
+    private String transferId;
+    private String exportName;
+    private String socket;
+    private String direction;
+    private String checkpointId;
+    private String file;
+    private ImageTransfer.Backend backend;
+    private int idleTimeoutSeconds;
+
+    public CreateImageTransferCommand() {
+    }
+
+    private CreateImageTransferCommand(String transferId, String direction, 
String socket, int idleTimeoutSeconds) {
+        this.transferId = transferId;
+        this.direction = direction;
+        this.socket = socket;
+        this.idleTimeoutSeconds = idleTimeoutSeconds;
+    }
+
+    public CreateImageTransferCommand(String transferId, String direction, 
String exportName, String socket, String checkpointId, int idleTimeoutSeconds) {
+        this(transferId, direction, socket, idleTimeoutSeconds);
+        this.backend = ImageTransfer.Backend.nbd;
+        this.exportName = exportName;
+        this.checkpointId = checkpointId;
+    }
+
+    public CreateImageTransferCommand(String transferId, String direction, 
String socket, String file, int idleTimeoutSeconds) {
+        this(transferId, direction, socket, idleTimeoutSeconds);
+        if (direction == ImageTransfer.Direction.download.toString()) {

Review Comment:
   String comparison uses `==`, so the download check can fail unexpectedly. 
Use `.equals(...)` (or parse `direction` into `ImageTransfer.Direction` and 
compare enums) to reliably reject the file backend for downloads.
   ```suggestion
           if (ImageTransfer.Direction.download.toString().equals(direction)) {
   ```



##########
plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetImageTransferProgressCommandWrapper.java:
##########
@@ -0,0 +1,95 @@
+//Licensed to the Apache Software Foundation (ASF) under one
+//or more contributor license agreements.  See the NOTICE file
+//distributed with this work for additional information
+//regarding copyright ownership.  The ASF licenses this file
+//to you under the Apache License, Version 2.0 (the
+//"License"); you may not use this file except in compliance
+//the License.  You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing,
+//software distributed under the License is distributed on an
+//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+//KIND, either express or implied.  See the License for the
+//specific language governing permissions and limitations
+//under the License.
+
+package com.cloud.hypervisor.kvm.resource.wrapper;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.cloudstack.backup.GetImageTransferProgressAnswer;
+import org.apache.cloudstack.backup.GetImageTransferProgressCommand;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import com.cloud.agent.api.Answer;
+import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
+import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk;
+import com.cloud.resource.CommandWrapper;
+import com.cloud.resource.ResourceWrapper;
+
+@ResourceWrapper(handles = GetImageTransferProgressCommand.class)
+public class LibvirtGetImageTransferProgressCommandWrapper extends 
CommandWrapper<GetImageTransferProgressCommand, Answer, 
LibvirtComputingResource> {
+    protected Logger logger = LogManager.getLogger(getClass());
+
+    @Override
+    public Answer execute(GetImageTransferProgressCommand cmd, 
LibvirtComputingResource resource) {
+        try {
+            List<String> transferIds = cmd.getTransferIds();
+            Map<String, String> volumePaths = cmd.getVolumePaths();
+            Map<String, Long> volumeSizes = cmd.getVolumeSizes();
+            Map<String, Long> progressMap = new HashMap<>();
+
+            if (transferIds == null || transferIds.isEmpty()) {
+                return new GetImageTransferProgressAnswer(cmd, true, "No 
transfers to check", progressMap);
+            }
+
+            for (String transferId : transferIds) {
+                String volumePath = volumePaths.get(transferId);
+                Long volumeSize = volumeSizes.get(transferId);
+
+                if (volumePath == null || volumeSize == null || volumeSize == 
0) {
+                    logger.warn("Missing volume path or size for transferId: 
{}", transferId);
+                    progressMap.put(transferId, null);
+                    continue;
+                }
+
+                try {
+                    File file = new File(volumePath);
+                    if (!file.exists()) {
+                        logger.warn("Volume file does not exist: {}", 
volumePath);
+                        progressMap.put(transferId, null);
+                        continue;
+                    }
+
+                    long currentSize = file.length();
+
+                    if (volumePath.endsWith(".qcow2") || 
volumePath.endsWith(".qcow")) {
+                        try {
+                            currentSize = 
KVMPhysicalDisk.getVirtualSizeFromFile(volumePath);
+                        } catch (Exception e) {
+                            logger.warn("Failed to get virtual size for qcow2 
file: {}, using physical size", volumePath, e);
+                        }
+                    }
+                    progressMap.put(transferId, currentSize);
+                    logger.debug("Transfer {} progress, current: {})", 
transferId, currentSize, volumeSize);
+

Review Comment:
   `GetImageTransferProgressAnswer`/API field descriptions indicate progress is 
a percentage (0-100), but this implementation stores a byte size 
(physical/virtual size) in the progress map. Either compute and return a 
percent based on `volumeSize` (and clamp to 0..100), or rename/retarget the 
API/Answer field to clearly represent bytes transferred to avoid client 
misinterpretation.
   ```suggestion
   
                       long progress = Math.max(0L, Math.min(100L, (currentSize 
* 100L) / volumeSize));
                       progressMap.put(transferId, progress);
                       logger.debug("Transfer {} progress: {}% (current size: 
{}, total size: {})", transferId, progress, currentSize, volumeSize);
   ```



##########
plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetImageTransferProgressCommandWrapper.java:
##########
@@ -0,0 +1,95 @@
+//Licensed to the Apache Software Foundation (ASF) under one
+//or more contributor license agreements.  See the NOTICE file
+//distributed with this work for additional information
+//regarding copyright ownership.  The ASF licenses this file
+//to you under the Apache License, Version 2.0 (the
+//"License"); you may not use this file except in compliance
+//the License.  You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing,
+//software distributed under the License is distributed on an
+//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+//KIND, either express or implied.  See the License for the
+//specific language governing permissions and limitations
+//under the License.
+
+package com.cloud.hypervisor.kvm.resource.wrapper;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.cloudstack.backup.GetImageTransferProgressAnswer;
+import org.apache.cloudstack.backup.GetImageTransferProgressCommand;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import com.cloud.agent.api.Answer;
+import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
+import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk;
+import com.cloud.resource.CommandWrapper;
+import com.cloud.resource.ResourceWrapper;
+
+@ResourceWrapper(handles = GetImageTransferProgressCommand.class)
+public class LibvirtGetImageTransferProgressCommandWrapper extends 
CommandWrapper<GetImageTransferProgressCommand, Answer, 
LibvirtComputingResource> {
+    protected Logger logger = LogManager.getLogger(getClass());
+
+    @Override
+    public Answer execute(GetImageTransferProgressCommand cmd, 
LibvirtComputingResource resource) {
+        try {
+            List<String> transferIds = cmd.getTransferIds();
+            Map<String, String> volumePaths = cmd.getVolumePaths();
+            Map<String, Long> volumeSizes = cmd.getVolumeSizes();
+            Map<String, Long> progressMap = new HashMap<>();
+
+            if (transferIds == null || transferIds.isEmpty()) {
+                return new GetImageTransferProgressAnswer(cmd, true, "No 
transfers to check", progressMap);
+            }
+
+            for (String transferId : transferIds) {
+                String volumePath = volumePaths.get(transferId);
+                Long volumeSize = volumeSizes.get(transferId);
+
+                if (volumePath == null || volumeSize == null || volumeSize == 
0) {
+                    logger.warn("Missing volume path or size for transferId: 
{}", transferId);
+                    progressMap.put(transferId, null);
+                    continue;
+                }
+
+                try {
+                    File file = new File(volumePath);
+                    if (!file.exists()) {
+                        logger.warn("Volume file does not exist: {}", 
volumePath);
+                        progressMap.put(transferId, null);
+                        continue;
+                    }
+
+                    long currentSize = file.length();
+
+                    if (volumePath.endsWith(".qcow2") || 
volumePath.endsWith(".qcow")) {
+                        try {
+                            currentSize = 
KVMPhysicalDisk.getVirtualSizeFromFile(volumePath);
+                        } catch (Exception e) {
+                            logger.warn("Failed to get virtual size for qcow2 
file: {}, using physical size", volumePath, e);
+                        }
+                    }
+                    progressMap.put(transferId, currentSize);
+                    logger.debug("Transfer {} progress, current: {})", 
transferId, currentSize, volumeSize);

Review Comment:
   The debug log format string has mismatched braces/parentheses and an 
incorrect number of placeholders for the arguments provided, which will log 
incorrectly and can confuse troubleshooting. Fix the format string to match the 
provided values (e.g., include both current and total with matching `{}` 
placeholders).
   ```suggestion
                       logger.debug("Transfer {} progress, current: {}, total: 
{}", transferId, currentSize, volumeSize);
   ```



##########
plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtDeleteVmCheckpointCommandWrapper.java:
##########
@@ -0,0 +1,80 @@
+//Licensed to the Apache Software Foundation (ASF) under one
+//or more contributor license agreements.  See the NOTICE file
+//distributed with this work for additional information
+//regarding copyright ownership.  The ASF licenses this file
+//to you under the Apache License, Version 2.0 (the
+//"License"); you may not use this file except in compliance
+//the License.  You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing,
+//software distributed under the License is distributed on an
+//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+//KIND, either express or implied.  See the License for the
+//specific language governing permissions and limitations
+//under the License.
+
+package com.cloud.hypervisor.kvm.resource.wrapper;
+
+import java.util.Map;
+
+import org.apache.cloudstack.backup.DeleteVmCheckpointCommand;
+
+import com.cloud.agent.api.Answer;
+import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
+import com.cloud.resource.CommandWrapper;
+import com.cloud.resource.ResourceWrapper;
+import com.cloud.utils.script.Script;
+
+@ResourceWrapper(handles = DeleteVmCheckpointCommand.class)
+public class LibvirtDeleteVmCheckpointCommandWrapper extends 
CommandWrapper<DeleteVmCheckpointCommand, Answer, LibvirtComputingResource> {
+
+    @Override
+    public Answer execute(DeleteVmCheckpointCommand cmd, 
LibvirtComputingResource resource) {
+        if (cmd.isStoppedVM()) {
+            return deleteBitmapsOnDisks(cmd);
+        }
+        return deleteDomainCheckpoint(cmd);
+    }
+
+    private Answer deleteDomainCheckpoint(DeleteVmCheckpointCommand cmd) {
+        String vmName = cmd.getVmName();
+        String checkpointId = cmd.getCheckpointId();
+        String virshCmd = String.format("virsh checkpoint-delete %s %s", 
vmName, checkpointId);
+        Script script = new Script("/bin/bash");
+        script.add("-c");
+        script.add(virshCmd);

Review Comment:
   Executing `virsh ...` via `bash -c` with unquoted/interpolated `vmName` and 
`checkpointId` risks shell argument injection if those values contain 
unexpected characters. Prefer invoking `virsh` with explicit arguments (no 
shell), and consider using the existing `CHECKPOINT_DELETE_COMMAND` pattern 
(including `--metadata`) to ensure consistent semantics.
   ```suggestion
           Script script = new Script("virsh");
           script.add("checkpoint-delete");
           script.add(vmName);
           script.add(checkpointId);
           script.add("--metadata");
   ```



##########
plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/JwtUtil.java:
##########
@@ -0,0 +1,57 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.cloudstack.veeam.utils;
+
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+public class JwtUtil {
+    public static final String ALGORITHM = "HmacSHA256";
+    public static final String ISSUER = "veeam-control";
+
+    public static String issueHs256Jwt(String subject, String scope, long 
ttlSeconds, String secret) throws Exception {
+        long now = Instant.now().getEpochSecond();
+        long exp = now + ttlSeconds;
+
+        String headerJson = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
+        String payloadJson =
+                "{"
+                        + "\"iss\":\"" + DataUtil.jsonEscape(ISSUER) + "\","
+                        + "\"sub\":\"" + DataUtil.jsonEscape(subject) + "\","
+                        + "\"scope\":\"" + DataUtil.jsonEscape(scope) + "\","
+                        + "\"iat\":" + now + ","
+                        + "\"exp\":" + exp
+                        + "}";
+

Review Comment:
   The JWT JSON is being constructed manually with a minimal escape function; 
this can produce invalid JSON for control characters (e.g., newlines) and is 
easy to get wrong over time. Prefer building the payload with Jackson (already 
present in the module) or using a vetted JWT library to avoid subtle 
encoding/escaping/security issues.
   ```suggestion
   import java.util.LinkedHashMap;
   import java.util.Map;
   
   import javax.crypto.Mac;
   import javax.crypto.spec.SecretKeySpec;
   
   import com.fasterxml.jackson.databind.ObjectMapper;
   
   public class JwtUtil {
       public static final String ALGORITHM = "HmacSHA256";
       public static final String ISSUER = "veeam-control";
       private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
   
       public static String issueHs256Jwt(String subject, String scope, long 
ttlSeconds, String secret) throws Exception {
           long now = Instant.now().getEpochSecond();
           long exp = now + ttlSeconds;
   
           Map<String, Object> headerClaims = new LinkedHashMap<>();
           headerClaims.put("alg", "HS256");
           headerClaims.put("typ", "JWT");
   
           Map<String, Object> payloadClaims = new LinkedHashMap<>();
           payloadClaims.put("iss", ISSUER);
           payloadClaims.put("sub", subject);
           payloadClaims.put("scope", scope);
           payloadClaims.put("iat", now);
           payloadClaims.put("exp", exp);
   
           String headerJson = OBJECT_MAPPER.writeValueAsString(headerClaims);
           String payloadJson = OBJECT_MAPPER.writeValueAsString(payloadClaims);
   ```



##########
plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/DataCenterJoinVOToDataCenterConverter.java:
##########
@@ -0,0 +1,78 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.cloudstack.veeam.api.converter;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.cloudstack.veeam.VeeamControlService;
+import org.apache.cloudstack.veeam.api.DataCentersRouteHandler;
+import org.apache.cloudstack.veeam.api.dto.DataCenter;
+import org.apache.cloudstack.veeam.api.dto.Link;
+import org.apache.cloudstack.veeam.api.dto.Ref;
+import org.apache.cloudstack.veeam.api.dto.SupportedVersions;
+import org.apache.cloudstack.veeam.api.dto.Version;
+
+import com.cloud.api.query.vo.DataCenterJoinVO;
+import com.cloud.org.Grouping;
+
+public class DataCenterJoinVOToDataCenterConverter {
+    public static DataCenter toDataCenter(final DataCenterJoinVO zone) {
+        final String id = zone.getUuid();
+        final String basePath = VeeamControlService.ContextPath.value();
+        final String href = basePath + DataCentersRouteHandler.BASE_ROUTE + 
DataCentersRouteHandler.BASE_ROUTE + "/" + id;
+
+        final DataCenter dc = new DataCenter();
+
+        // ---- Identity ----
+        dc.setId(id);
+        dc.setHref(href);
+        dc.setName(zone.getName());
+        dc.setDescription(zone.getDescription());
+
+        // ---- State ----
+        
dc.setStatus(Grouping.AllocationState.Enabled.equals(zone.getAllocationState()) 
? "up" : "down");
+        dc.setLocal("false");
+        dc.setQuotaMode("disabled");
+        dc.setStorageFormat("v5");
+
+        // ---- Versions ----
+        final Version ver = Version.fromPackageAndCSVersion(false);
+        dc.setVersion(ver);
+        dc.setSupportedVersions(new SupportedVersions(List.of(ver)));
+
+        // ---- mac_pool (static placeholder) ----
+        dc.setMacPool(Ref.of(basePath + "/macpools/default", "default"));
+
+        // ---- Related links ----
+        dc.link = Arrays.asList(
+                Link.of(href + "/clusters", "clusters"),
+                Link.of(href + "/networks", "networks"),
+                Link.of(href + "/storagedomains", "storagedomains")
+        );

Review Comment:
   The `href` concatenation duplicates `BASE_ROUTE`, producing an incorrect 
URL. Also, `Link.of(rel, href)` is being called with parameters reversed (rel 
is being set to a URL and href to a literal like `clusters`). Build `href` as 
`basePath + BASE_ROUTE + \"/\" + id`, and construct links as 
`Link.of(\"clusters\", href + \"/clusters\")` (same for 
networks/storagedomains).



##########
api/src/main/java/org/apache/cloudstack/api/response/BackupResponse.java:
##########
@@ -127,6 +127,18 @@ public class BackupResponse extends BaseResponse {
     @Param(description = "Indicates whether the VM from which the backup was 
taken is expunged or not", since = "4.22.0")
     private Boolean isVmExpunged;
 
+    @SerializedName("from_checkpoint_id")
+    @Param(description = "Previous active checkpoint id for incremental 
backups", since = "4.22.0")
+    private String fromCheckpointId;
+
+    @SerializedName("to_checkpoint_id")

Review Comment:
   These new response field names use underscores (`from_checkpoint_id`, 
`to_checkpoint_id`), which is inconsistent with typical CloudStack API response 
naming (commonly lower-cased without underscores like `backupid`, `vmid`, 
etc.). Consider aligning the serialized names with existing conventions to 
minimize client surprises and maintain consistency.
   ```suggestion
       @SerializedName("fromcheckpointid")
       @Param(description = "Previous active checkpoint id for incremental 
backups", since = "4.22.0")
       private String fromCheckpointId;
   
       @SerializedName("tocheckpointid")
   ```



##########
plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetImageTransferProgressCommandWrapper.java:
##########
@@ -0,0 +1,95 @@
+//Licensed to the Apache Software Foundation (ASF) under one
+//or more contributor license agreements.  See the NOTICE file
+//distributed with this work for additional information
+//regarding copyright ownership.  The ASF licenses this file
+//to you under the Apache License, Version 2.0 (the
+//"License"); you may not use this file except in compliance
+//the License.  You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing,
+//software distributed under the License is distributed on an
+//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+//KIND, either express or implied.  See the License for the
+//specific language governing permissions and limitations
+//under the License.
+
+package com.cloud.hypervisor.kvm.resource.wrapper;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.cloudstack.backup.GetImageTransferProgressAnswer;
+import org.apache.cloudstack.backup.GetImageTransferProgressCommand;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import com.cloud.agent.api.Answer;
+import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
+import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk;
+import com.cloud.resource.CommandWrapper;
+import com.cloud.resource.ResourceWrapper;
+
+@ResourceWrapper(handles = GetImageTransferProgressCommand.class)
+public class LibvirtGetImageTransferProgressCommandWrapper extends 
CommandWrapper<GetImageTransferProgressCommand, Answer, 
LibvirtComputingResource> {
+    protected Logger logger = LogManager.getLogger(getClass());
+
+    @Override
+    public Answer execute(GetImageTransferProgressCommand cmd, 
LibvirtComputingResource resource) {
+        try {
+            List<String> transferIds = cmd.getTransferIds();
+            Map<String, String> volumePaths = cmd.getVolumePaths();
+            Map<String, Long> volumeSizes = cmd.getVolumeSizes();
+            Map<String, Long> progressMap = new HashMap<>();
+
+            if (transferIds == null || transferIds.isEmpty()) {
+                return new GetImageTransferProgressAnswer(cmd, true, "No 
transfers to check", progressMap);
+            }
+
+            for (String transferId : transferIds) {
+                String volumePath = volumePaths.get(transferId);
+                Long volumeSize = volumeSizes.get(transferId);
+
+                if (volumePath == null || volumeSize == null || volumeSize == 
0) {
+                    logger.warn("Missing volume path or size for transferId: 
{}", transferId);
+                    progressMap.put(transferId, null);
+                    continue;
+                }
+
+                try {
+                    File file = new File(volumePath);
+                    if (!file.exists()) {
+                        logger.warn("Volume file does not exist: {}", 
volumePath);
+                        progressMap.put(transferId, null);
+                        continue;
+                    }
+
+                    long currentSize = file.length();
+
+                    if (volumePath.endsWith(".qcow2") || 
volumePath.endsWith(".qcow")) {
+                        try {
+                            currentSize = 
KVMPhysicalDisk.getVirtualSizeFromFile(volumePath);

Review Comment:
   `GetImageTransferProgressAnswer`/API field descriptions indicate progress is 
a percentage (0-100), but this implementation stores a byte size 
(physical/virtual size) in the progress map. Either compute and return a 
percent based on `volumeSize` (and clamp to 0..100), or rename/retarget the 
API/Answer field to clearly represent bytes transferred to avoid client 
misinterpretation.



##########
plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/PathUtil.java:
##########
@@ -0,0 +1,75 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.cloudstack.veeam.utils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.lang3.StringUtils;
+
+import com.cloud.utils.UuidUtils;
+
+public class PathUtil {
+    private static final boolean CONSIDER_ONLY_UUID_AS_ID = false;
+
+    public static List<String> extractIdAndSubPath(final String path, final 
String baseRoute) {
+
+        if (StringUtils.isBlank(path)) {
+                return null;
+            }
+
+            // Remove base route (be tolerant of trailing slash in baseRoute)
+            String rest = path;
+            if (StringUtils.isNotBlank(baseRoute)) {
+                String normalizedBase = baseRoute.endsWith("/") && 
baseRoute.length() > 1
+                        ? baseRoute.substring(0, baseRoute.length() - 1)
+                        : baseRoute;
+                if (rest.startsWith(normalizedBase)) {
+                    rest = rest.substring(normalizedBase.length());
+                }
+            }
+
+            // Expect "/{id}" or "/{id}/..." (no empty segments)
+            if (StringUtils.isBlank(rest) || !rest.startsWith("/")) {
+                return null; // /api/datacenters (no id) or invalid format
+            }
+
+            rest = rest.substring(1); // remove leading '/'
+
+            if (StringUtils.isBlank(rest)) {
+                return null;
+            }
+
+            final String[] parts = rest.split("/", -1);
+
+            // Collect non-blank segments
+            List<String> validParts = new ArrayList<>();
+            for (String part : parts) {
+                if (StringUtils.isNotBlank(part)) {
+                    validParts.add(part);
+                }
+            }
+
+            // Validate first segment is a UUID
+            if (validParts.isEmpty() || (CONSIDER_ONLY_UUID_AS_ID && 
!UuidUtils.isUuid(validParts.get(0)))) {
+                return null;
+            }

Review Comment:
   The indentation is inconsistent, and the UUID validation comment does not 
match behavior when `CONSIDER_ONLY_UUID_AS_ID` is `false` (no UUID validation 
occurs). Consider either enabling UUID validation (and enforcing it), or 
updating the comment/logic to reflect the intended contract; also prefer 
returning an empty list (or an Optional) instead of `null` to simplify callers.



##########
plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/PathUtil.java:
##########
@@ -0,0 +1,75 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.cloudstack.veeam.utils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.lang3.StringUtils;
+
+import com.cloud.utils.UuidUtils;
+
+public class PathUtil {
+    private static final boolean CONSIDER_ONLY_UUID_AS_ID = false;
+
+    public static List<String> extractIdAndSubPath(final String path, final 
String baseRoute) {
+
+        if (StringUtils.isBlank(path)) {
+                return null;
+            }
+
+            // Remove base route (be tolerant of trailing slash in baseRoute)
+            String rest = path;
+            if (StringUtils.isNotBlank(baseRoute)) {
+                String normalizedBase = baseRoute.endsWith("/") && 
baseRoute.length() > 1
+                        ? baseRoute.substring(0, baseRoute.length() - 1)
+                        : baseRoute;
+                if (rest.startsWith(normalizedBase)) {
+                    rest = rest.substring(normalizedBase.length());
+                }
+            }
+
+            // Expect "/{id}" or "/{id}/..." (no empty segments)
+            if (StringUtils.isBlank(rest) || !rest.startsWith("/")) {
+                return null; // /api/datacenters (no id) or invalid format
+            }
+
+            rest = rest.substring(1); // remove leading '/'
+
+            if (StringUtils.isBlank(rest)) {
+                return null;
+            }
+
+            final String[] parts = rest.split("/", -1);
+
+            // Collect non-blank segments
+            List<String> validParts = new ArrayList<>();
+            for (String part : parts) {
+                if (StringUtils.isNotBlank(part)) {
+                    validParts.add(part);
+                }
+            }
+
+            // Validate first segment is a UUID
+            if (validParts.isEmpty() || (CONSIDER_ONLY_UUID_AS_ID && 
!UuidUtils.isUuid(validParts.get(0)))) {
+                return null;
+            }
+
+            return validParts;

Review Comment:
   The indentation is inconsistent, and the UUID validation comment does not 
match behavior when `CONSIDER_ONLY_UUID_AS_ID` is `false` (no UUID validation 
occurs). Consider either enabling UUID validation (and enforcing it), or 
updating the comment/logic to reflect the intended contract; also prefer 
returning an empty list (or an Optional) instead of `null` to simplify callers.
   ```suggestion
           if (StringUtils.isBlank(path)) {
               return new ArrayList<>();
           }
   
           // Remove base route (be tolerant of trailing slash in baseRoute)
           String rest = path;
           if (StringUtils.isNotBlank(baseRoute)) {
               String normalizedBase = baseRoute.endsWith("/") && 
baseRoute.length() > 1
                       ? baseRoute.substring(0, baseRoute.length() - 1)
                       : baseRoute;
               if (rest.startsWith(normalizedBase)) {
                   rest = rest.substring(normalizedBase.length());
               }
           }
   
           // Expect "/{id}" or "/{id}/..." (no empty segments)
           if (StringUtils.isBlank(rest) || !rest.startsWith("/")) {
               return new ArrayList<>(); // /api/datacenters (no id) or invalid 
format
           }
   
           rest = rest.substring(1); // remove leading '/'
   
           if (StringUtils.isBlank(rest)) {
               return new ArrayList<>();
           }
   
           final String[] parts = rest.split("/", -1);
   
           // Collect non-blank segments
           List<String> validParts = new ArrayList<>();
           for (String part : parts) {
               if (StringUtils.isNotBlank(part)) {
                   validParts.add(part);
               }
           }
   
           // Ensure the first segment exists; validate it as a UUID only when 
configured to do so
           if (validParts.isEmpty() || (CONSIDER_ONLY_UUID_AS_ID && 
!UuidUtils.isUuid(validParts.get(0)))) {
               return new ArrayList<>();
           }
   
           return validParts;
   ```



##########
plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/utils/ResponseWriter.java:
##########
@@ -0,0 +1,83 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.cloudstack.veeam.utils;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.cloudstack.veeam.api.dto.Fault;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+public final class ResponseWriter {
+    private static final Logger LOGGER = 
LogManager.getLogger(ResponseWriter.class);
+
+    private final Mapper mapper;
+
+    public ResponseWriter(final Mapper mapper) {
+        this.mapper = mapper;
+    }
+
+    public void write(final HttpServletResponse resp, final int status, final 
Object body, final Negotiation.OutFormat fmt)
+            throws IOException {
+
+        resp.setStatus(status);
+
+        if (body == null) {
+            resp.setContentLength(0);
+            return;
+        }
+
+        final String payload;
+        final String contentType;
+
+        try {
+            if (fmt == Negotiation.OutFormat.XML) {
+                contentType = "application/xml";
+                payload = mapper.toXml(body);
+            } else {
+                contentType = "application/json";
+                payload = mapper.toJson(body);
+            }
+        } catch (Exception e) {
+            // Last-resort fallback
+            resp.setStatus(500);
+            resp.setHeader("Content-Type", "text/plain");
+            resp.getWriter().write("Internal Server Error");
+            return;
+        }
+
+        LOGGER.info("Writing response: {}\n{}", status, payload);

Review Comment:
   Logging full response payloads at INFO can leak sensitive data (tokens, IDs, 
credentials) and can be very noisy/large in production. Consider switching to 
DEBUG and/or logging only metadata (status, content-type, payload length) with 
optional truncation.
   ```suggestion
           LOGGER.debug("Writing response: status={}, contentType={}, 
payloadLength={}", status, contentType, payload.length());
   ```



##########
plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/ReportedDevice.java:
##########
@@ -0,0 +1,84 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.cloudstack.veeam.api.dto;
+
+public class ReportedDevice extends BaseDto {
+    private String comment;
+    private String description;
+    private NamedList<Ip> ips;
+    private Mac Mac;

Review Comment:
   Field name `Mac` is capitalized, which is inconsistent with Java conventions 
and makes the code harder to read/maintain. Rename the field to `mac` and 
update getters/setters accordingly (method names can remain `getMac/setMac`).



##########
plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/dto/ReportedDevice.java:
##########
@@ -0,0 +1,84 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.cloudstack.veeam.api.dto;
+
+public class ReportedDevice extends BaseDto {
+    private String comment;
+    private String description;
+    private NamedList<Ip> ips;
+    private Mac Mac;
+    private String name;
+    private String type;
+    private Vm vm;
+
+    public String getComment() {
+        return comment;
+    }
+
+    public void setComment(String comment) {
+        this.comment = comment;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public NamedList<Ip> getIps() {
+        return ips;
+    }
+
+    public void setIps(NamedList<Ip> ips) {
+        this.ips = ips;
+    }
+
+    public Mac getMac() {
+        return Mac;
+    }
+
+    public void setMac(Mac mac) {
+        Mac = mac;
+    }

Review Comment:
   Field name `Mac` is capitalized, which is inconsistent with Java conventions 
and makes the code harder to read/maintain. Rename the field to `mac` and 
update getters/setters accordingly (method names can remain `getMac/setMac`).



##########
plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStopNBDServerCommandWrapper.java:
##########
@@ -0,0 +1,72 @@
+//Licensed to the Apache Software Foundation (ASF) under one
+//or more contributor license agreements.  See the NOTICE file
+//distributed with this work for additional information
+//regarding copyright ownership.  The ASF licenses this file
+//to you under the Apache License, Version 2.0 (the
+//"License"); you may not use this file except in compliance
+//the License.  You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing,
+//software distributed under the License is distributed on an
+//"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+//KIND, either express or implied.  See the License for the
+//specific language governing permissions and limitations
+//under the License.
+
+package com.cloud.hypervisor.kvm.resource.wrapper;
+
+import org.apache.cloudstack.backup.StopNBDServerCommand;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import com.cloud.agent.api.Answer;
+import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
+import com.cloud.resource.CommandWrapper;
+import com.cloud.resource.ResourceWrapper;
+import com.cloud.utils.script.Script;
+
+@ResourceWrapper(handles = StopNBDServerCommand.class)
+public class LibvirtStopNBDServerCommandWrapper extends 
CommandWrapper<StopNBDServerCommand, Answer, LibvirtComputingResource> {
+    protected Logger logger = LogManager.getLogger(getClass());
+
+    private void resetService(String unitName) {
+        Script resetScript = new Script("/bin/bash", logger);
+        resetScript.add("-c");
+        resetScript.add(String.format("systemctl reset-failed %s || true", 
unitName));
+        resetScript.execute();
+    }
+
+    @Override
+    public Answer execute(StopNBDServerCommand cmd, LibvirtComputingResource 
resource) {
+        try {
+            String unitName = "qemu-nbd-" + cmd.getTransferId().hashCode();

Review Comment:
   Using `hashCode()` for a systemd unit name introduces a collision risk 
(different transferIds can produce the same hash), which could stop/reset the 
wrong service. Prefer deriving the unit name from the actual transfer ID 
(sanitized) or using a collision-resistant encoding (e.g., base32/base64url of 
the UUID) with a safe length limit.



##########
plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/filter/AllowedClientCidrsFilter.java:
##########
@@ -0,0 +1,100 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.cloudstack.veeam.filter;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.List;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.cloudstack.veeam.VeeamControlService;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import com.cloud.utils.net.NetUtils;
+
+public class AllowedClientCidrsFilter implements Filter {
+
+    private static final Logger LOGGER = 
LogManager.getLogger(AllowedClientCidrsFilter.class);
+
+    private final VeeamControlService veeamControlService;
+
+    public AllowedClientCidrsFilter(VeeamControlService veeamControlService) {
+        this.veeamControlService = veeamControlService;
+    }
+
+    @Override
+    public void init(FilterConfig filterConfig) {
+        // no-op
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, 
FilterChain chain)
+            throws IOException, ServletException {
+        if (!(request instanceof HttpServletRequest) || !(response instanceof 
HttpServletResponse)) {
+            chain.doFilter(request, response);
+            return;
+        }
+
+        final HttpServletRequest req = (HttpServletRequest) request;
+        final HttpServletResponse resp = (HttpServletResponse) response;
+
+        if (veeamControlService == null) {
+            LOGGER.warn("Failed to inject VeeamControlService, allowing 
request by default");
+            chain.doFilter(request, response);

Review Comment:
   Failing open when `VeeamControlService` injection is missing makes the 
allow-list enforcement ineffective and can unintentionally expose the endpoint. 
Prefer failing closed (e.g., return 503/500) or at least deny with 403 when 
CIDR enforcement cannot be evaluated.
   ```suggestion
               LOGGER.error("Failed to inject VeeamControlService, rejecting 
request because allowed client CIDRs cannot be evaluated");
               resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, 
"Service Unavailable");
   ```



##########
plugins/integrations/veeam-control-service/src/main/java/org/apache/cloudstack/veeam/api/converter/AsyncJobJoinVOToJobConverter.java:
##########
@@ -0,0 +1,90 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.cloudstack.veeam.api.converter;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.cloudstack.jobs.JobInfo;
+import org.apache.cloudstack.veeam.VeeamControlService;
+import org.apache.cloudstack.veeam.api.JobsRouteHandler;
+import org.apache.cloudstack.veeam.api.dto.Job;
+import org.apache.cloudstack.veeam.api.dto.Ref;
+import org.apache.cloudstack.veeam.api.dto.ResourceAction;
+import org.apache.cloudstack.veeam.api.dto.VmAction;
+
+import com.cloud.api.query.vo.AsyncJobJoinVO;
+import com.cloud.api.query.vo.UserVmJoinVO;
+
+public class AsyncJobJoinVOToJobConverter {
+
+    public static Job toJob(AsyncJobJoinVO vo) {
+        Job job = new Job();
+        final String basePath = VeeamControlService.ContextPath.value();
+        job.setId(vo.getUuid());
+        job.setHref(basePath + JobsRouteHandler.BASE_ROUTE + "/" + 
vo.getUuid());
+        job.setAutoCleared(Boolean.TRUE.toString());
+        job.setExternal(Boolean.TRUE.toString());
+        job.setLastUpdated(System.currentTimeMillis());
+        job.setStartTime(vo.getCreated().getTime());
+        JobInfo.Status status = JobInfo.Status.values()[vo.getStatus()];
+        Long endTime = System.currentTimeMillis();
+        if (status == JobInfo.Status.SUCCEEDED) {
+            job.setStatus("finished");
+            job.setEndTime(System.currentTimeMillis());
+        } else if (status == JobInfo.Status.FAILED) {
+            job.setStatus(status.name().toLowerCase());
+        } else if (status == JobInfo.Status.CANCELLED) {
+            job.setStatus("aborted");
+        } else {
+            job.setStatus("started");
+            endTime = null;
+        }
+        if (endTime != null) {
+            job.setEndTime(endTime);
+        }
+        job.setOwner(Ref.of(basePath + "/api/users/" + vo.getUserUuid(), 
vo.getUserUuid()));
+        job.setDescription("Something");
+        job.setLink(Collections.emptyList());
+        return job;
+    }
+
+    public static List<Job> toJobList(List<AsyncJobJoinVO> vos) {
+        return 
vos.stream().map(AsyncJobJoinVOToJobConverter::toJob).collect(Collectors.toList());
+    }
+
+    protected static void fillAction(final ResourceAction action, final 
AsyncJobJoinVO vo) {
+        final String basePath = VeeamControlService.ContextPath.value();
+        action.setJob(Ref.of(basePath + JobsRouteHandler.BASE_ROUTE + 
vo.getUuid(), vo.getUuid()));

Review Comment:
   The job href is missing a `/` separator between `BASE_ROUTE` and the UUID, 
producing malformed URLs. Use `basePath + JobsRouteHandler.BASE_ROUTE + \"/\" + 
vo.getUuid()` (consistent with `toJob(...)`).
   ```suggestion
           action.setJob(Ref.of(basePath + JobsRouteHandler.BASE_ROUTE + "/" + 
vo.getUuid(), vo.getUuid()));
   ```



##########
scripts/vm/hypervisor/kvm/imageserver/__main__.py:
##########
@@ -0,0 +1,20 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from .server import main
+
+main()

Review Comment:
   Calling `main()` unconditionally at import-time can trigger side effects if 
`imageserver.__main__` is imported indirectly (tests/tools). Prefer guarding 
with `if __name__ == \"__main__\": main()` so it only starts when executed as a 
script/module.
   ```suggestion
   if __name__ == "__main__":
       main()
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to