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


##########
plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapperTest.java:
##########
@@ -579,4 +584,123 @@ public void testExecuteWithMultipleVolumes() throws 
Exception {
             }
         }
     }
+
+    private String invokeGetDeviceToAttachDisk(String vmName) throws Exception 
{
+        Method method = 
LibvirtRestoreBackupCommandWrapper.class.getDeclaredMethod("getDeviceToAttachDisk",
 String.class);
+        method.setAccessible(true);
+        try {
+            return (String) method.invoke(wrapper, vmName);
+        } catch (java.lang.reflect.InvocationTargetException e) {
+            throw (Exception) e.getCause();
+        }
+    }
+
+    private String[] captureAttachCommand(Storage.StoragePoolType poolType) 
throws Exception {
+        PrimaryDataStoreTO volumePool = Mockito.mock(PrimaryDataStoreTO.class);
+        lenient().when(volumePool.getPoolType()).thenReturn(poolType);
+        lenient().when(volumePool.getHost()).thenReturn("10.0.0.1");
+        lenient().when(volumePool.getUuid()).thenReturn("pool-uuid");
+        KVMStoragePoolManager storagePoolMgr = 
Mockito.mock(KVMStoragePoolManager.class);
+        KVMStoragePool primaryPool = Mockito.mock(KVMStoragePool.class);
+        lenient().when(storagePoolMgr.getStoragePool(any(), 
anyString())).thenReturn(primaryPool);
+        lenient().when(primaryPool.getAuthUserName()).thenReturn("cloudstack");
+
+        Method method = 
LibvirtRestoreBackupCommandWrapper.class.getDeclaredMethod("attachVolumeToVm",
+                KVMStoragePoolManager.class, String.class, 
PrimaryDataStoreTO.class, String.class);
+        method.setAccessible(true);
+
+        final String[][] captured = new String[1][];
+        try (MockedStatic<Script> scriptMock = mockStatic(Script.class)) {
+            scriptMock.when(() -> 
Script.getExecutableAbsolutePath(anyString()))
+                    .thenAnswer(invocation -> invocation.getArgument(0));
+            scriptMock.when(() -> Script.executePipedCommands(anyList(), 
anyLong()))
+                    .thenReturn(new Pair<>(0, "vda" + System.lineSeparator()));
+            scriptMock.when(() -> 
Script.executeCommandForExitValue(any(String[].class)))
+                    .thenAnswer(invocation -> {
+                        // Mockito expands varargs, so the command comes back 
as individual arguments.
+                        captured[0] = 
Arrays.stream(invocation.getArguments()).map(String::valueOf).toArray(String[]::new);
+                        return 0;
+                    });
+            method.invoke(wrapper, storagePoolMgr, "test-vm", volumePool, 
"/path/to/volume");
+        }

Review Comment:
   captureAttachCommand() indirectly triggers real filesystem I/O for the RBD 
path (Files.createTempFile/write/deleteIfExists) because those calls are not 
mocked here. This is inconsistent with the rest of this test class (which mocks 
Files) and can make the unit test flaky or fail in restricted CI environments. 
Mock these Files calls inside captureAttachCommand so the attach tests stay 
hermetic.



##########
plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java:
##########
@@ -340,38 +341,68 @@ private boolean 
replaceBlockDeviceWithBackup(KVMStoragePoolManager storagePoolMg
 
     private boolean attachVolumeToVm(KVMStoragePoolManager storagePoolMgr, 
String vmName, PrimaryDataStoreTO volumePool, String volumePath) {
         String deviceToAttachDiskTo = getDeviceToAttachDisk(vmName);
+        if (Storage.StoragePoolType.RBD.equals(volumePool.getPoolType())) {
+            return attachRbdVolumeToVm(storagePoolMgr, vmName, volumePool, 
volumePath, deviceToAttachDiskTo);
+        }
         List<String> virshCmd = new ArrayList<>();
         virshCmd.add(Script.getExecutableAbsolutePath("virsh"));
-        if (volumePool.getPoolType() == Storage.StoragePoolType.RBD) {
-            String xmlForRbdDisk = getXmlForRbdDisk(storagePoolMgr, 
volumePool, volumePath, deviceToAttachDiskTo);
-            logger.debug("RBD disk xml to attach: {}", xmlForRbdDisk);
-            virshCmd.add("attach-device");
-            virshCmd.add(vmName);
-            virshCmd.add("/dev/stdin");
-            virshCmd.add("<<EOF%sEOF");
-        } else {
-            virshCmd.add("attach-disk");
-            virshCmd.add(vmName);
-            virshCmd.add(volumePath);
-            virshCmd.add(deviceToAttachDiskTo);
-            if 
(Storage.StoragePoolType.Linstor.equals(volumePool.getPoolType())) {
-                virshCmd.add("--subdriver");
-                virshCmd.add("qcow2");
-            }
-            virshCmd.add("--cache");
-            virshCmd.add("none");
+        virshCmd.add("attach-disk");
+        virshCmd.add(vmName);
+        virshCmd.add(volumePath);
+        virshCmd.add(deviceToAttachDiskTo);
+        virshCmd.add("--driver");
+        virshCmd.add("qemu");
+        if (!Storage.StoragePoolType.Linstor.equals(volumePool.getPoolType())) 
{
+            virshCmd.add("--subdriver");
+            virshCmd.add("qcow2");
         }
+        virshCmd.add("--cache");
+        virshCmd.add("none");
         int exitValue = Script.executeCommandForExitValue(virshCmd.toArray(new 
String[0]));
         return exitValue == 0;
     }
 
+    private boolean attachRbdVolumeToVm(KVMStoragePoolManager storagePoolMgr, 
String vmName, PrimaryDataStoreTO volumePool, String volumePath,
+            String deviceToAttachDiskTo) {
+        String xmlForRbdDisk = getXmlForRbdDisk(storagePoolMgr, volumePool, 
volumePath, deviceToAttachDiskTo);
+        logger.debug("RBD disk xml to attach: {}", xmlForRbdDisk);
+        // The command is executed without a shell, so the XML cannot be piped 
in through a
+        // here-document. Write it to a temporary file and pass virsh the path 
instead.
+        Path xmlFile = null;
+        try {
+            xmlFile = Files.createTempFile("csrestore-rbd-", ".xml");
+            Files.write(xmlFile, 
xmlForRbdDisk.getBytes(StandardCharsets.UTF_8));
+            String[] virshCmd = new String[] { 
Script.getExecutableAbsolutePath("virsh"), "attach-device", vmName, 
xmlFile.toString() };
+            return Script.executeCommandForExitValue(virshCmd) == 0;
+        } catch (IOException e) {
+            logger.error("Failed to write the RBD disk XML used to attach 
volume [{}] to VM [{}]", volumePath, vmName, e);
+            return false;
+        } finally {
+            if (xmlFile != null) {
+                try {
+                    Files.deleteIfExists(xmlFile);
+                } catch (IOException e) {
+                    logger.warn("Failed to delete the temporary RBD disk XML 
file [{}].", xmlFile, e);
+                }
+            }
+        }
+    }
+
     private String getDeviceToAttachDisk(String vmName) {
         String[] domblkCmd = new String[] { 
Script.getExecutableAbsolutePath("virsh"), "domblklist", "--domain", vmName };
         String[] tailCmd = new String[] { 
Script.getExecutableAbsolutePath("tail"), "-n", "3" };
         String[] headCmd = new String[] { 
Script.getExecutableAbsolutePath("head"), "-n", "1" };
-        String[] awkCmd = new String[] { 
Script.getExecutableAbsolutePath("awk"), "'{print $1}'" };
+        // The commands are executed without a shell, so the awk program must 
be passed as a plain
+        // argument. Keeping the quotes a shell would have stripped makes awk 
fail with
+        // "invalid char" and produce no output.
+        String[] awkCmd = new String[] { 
Script.getExecutableAbsolutePath("awk"), "{print $1}" };
         Pair<Integer, String> result = 
Script.executePipedCommands(Arrays.asList(domblkCmd, tailCmd, headCmd, awkCmd), 
0);
-        String currentDevice = result.second();
+        // executePipedCommands appends a line separator to every line it 
reads, so the device
+        // name has to be trimmed before the last character can be incremented.
+        String currentDevice = result.second() == null ? "" : 
result.second().trim();
+        if (result.first() == null || result.first() != 0 || 
StringUtils.isBlank(currentDevice)) {
+            throw new CloudRuntimeException(String.format("Failed to determine 
the device to attach the restored volume to on VM [%s].", vmName));
+        }

Review Comment:
   The exception thrown when device detection fails omits the command exit code 
and raw output, which makes restore-and-attach failures hard to diagnose 
(especially since execute() surfaces only the exception message). Include the 
exit value and raw stdout in the message before trimming so operators can see 
what domblklist/awk returned.



-- 
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