|
25 | 25 | import static org.mockito.Mockito.when; |
26 | 26 |
|
27 | 27 | import java.io.IOException; |
| 28 | +import java.lang.reflect.Method; |
28 | 29 | import java.nio.file.Files; |
29 | 30 | import java.nio.file.Path; |
30 | 31 | import java.util.Arrays; |
| 32 | +import java.util.List; |
31 | 33 |
|
32 | 34 | import org.apache.cloudstack.backup.BackupAnswer; |
33 | 35 | import org.apache.cloudstack.backup.RestoreBackupCommand; |
|
42 | 44 |
|
43 | 45 | import com.cloud.agent.api.Answer; |
44 | 46 | import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; |
| 47 | +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; |
| 48 | +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; |
45 | 49 | import com.cloud.storage.Storage; |
46 | 50 | import com.cloud.utils.Pair; |
| 51 | +import com.cloud.utils.exception.CloudRuntimeException; |
47 | 52 | import com.cloud.utils.script.Script; |
48 | 53 | import com.cloud.vm.VirtualMachine; |
49 | 54 |
|
@@ -579,4 +584,123 @@ public void testExecuteWithMultipleVolumes() throws Exception { |
579 | 584 | } |
580 | 585 | } |
581 | 586 | } |
| 587 | + |
| 588 | + private String invokeGetDeviceToAttachDisk(String vmName) throws Exception { |
| 589 | + Method method = LibvirtRestoreBackupCommandWrapper.class.getDeclaredMethod("getDeviceToAttachDisk", String.class); |
| 590 | + method.setAccessible(true); |
| 591 | + try { |
| 592 | + return (String) method.invoke(wrapper, vmName); |
| 593 | + } catch (java.lang.reflect.InvocationTargetException e) { |
| 594 | + throw (Exception) e.getCause(); |
| 595 | + } |
| 596 | + } |
| 597 | + |
| 598 | + private String[] captureAttachCommand(Storage.StoragePoolType poolType) throws Exception { |
| 599 | + PrimaryDataStoreTO volumePool = Mockito.mock(PrimaryDataStoreTO.class); |
| 600 | + lenient().when(volumePool.getPoolType()).thenReturn(poolType); |
| 601 | + lenient().when(volumePool.getHost()).thenReturn("10.0.0.1"); |
| 602 | + lenient().when(volumePool.getUuid()).thenReturn("pool-uuid"); |
| 603 | + KVMStoragePoolManager storagePoolMgr = Mockito.mock(KVMStoragePoolManager.class); |
| 604 | + KVMStoragePool primaryPool = Mockito.mock(KVMStoragePool.class); |
| 605 | + lenient().when(storagePoolMgr.getStoragePool(any(), anyString())).thenReturn(primaryPool); |
| 606 | + lenient().when(primaryPool.getAuthUserName()).thenReturn("cloudstack"); |
| 607 | + |
| 608 | + Method method = LibvirtRestoreBackupCommandWrapper.class.getDeclaredMethod("attachVolumeToVm", |
| 609 | + KVMStoragePoolManager.class, String.class, PrimaryDataStoreTO.class, String.class); |
| 610 | + method.setAccessible(true); |
| 611 | + |
| 612 | + final String[][] captured = new String[1][]; |
| 613 | + try (MockedStatic<Script> scriptMock = mockStatic(Script.class)) { |
| 614 | + scriptMock.when(() -> Script.getExecutableAbsolutePath(anyString())) |
| 615 | + .thenAnswer(invocation -> invocation.getArgument(0)); |
| 616 | + scriptMock.when(() -> Script.executePipedCommands(anyList(), anyLong())) |
| 617 | + .thenReturn(new Pair<>(0, "vda" + System.lineSeparator())); |
| 618 | + scriptMock.when(() -> Script.executeCommandForExitValue(any(String[].class))) |
| 619 | + .thenAnswer(invocation -> { |
| 620 | + // Mockito expands varargs, so the command comes back as individual arguments. |
| 621 | + captured[0] = Arrays.stream(invocation.getArguments()).map(String::valueOf).toArray(String[]::new); |
| 622 | + return 0; |
| 623 | + }); |
| 624 | + method.invoke(wrapper, storagePoolMgr, "test-vm", volumePool, "/path/to/volume"); |
| 625 | + } |
| 626 | + return captured[0]; |
| 627 | + } |
| 628 | + |
| 629 | + @Test |
| 630 | + public void testGetDeviceToAttachDiskTrimsOutputBeforeIncrementing() throws Exception { |
| 631 | + try (MockedStatic<Script> scriptMock = mockStatic(Script.class)) { |
| 632 | + scriptMock.when(() -> Script.getExecutableAbsolutePath(anyString())) |
| 633 | + .thenAnswer(invocation -> invocation.getArgument(0)); |
| 634 | + // executePipedCommands appends a line separator to each line it reads. |
| 635 | + scriptMock.when(() -> Script.executePipedCommands(anyList(), anyLong())) |
| 636 | + .thenReturn(new Pair<>(0, "vda" + System.lineSeparator())); |
| 637 | + |
| 638 | + Assert.assertEquals("vdb", invokeGetDeviceToAttachDisk("test-vm")); |
| 639 | + } |
| 640 | + } |
| 641 | + |
| 642 | + @Test |
| 643 | + public void testGetDeviceToAttachDiskPassesUnquotedAwkProgram() throws Exception { |
| 644 | + try (MockedStatic<Script> scriptMock = mockStatic(Script.class)) { |
| 645 | + scriptMock.when(() -> Script.getExecutableAbsolutePath(anyString())) |
| 646 | + .thenAnswer(invocation -> invocation.getArgument(0)); |
| 647 | + final List<String[]>[] captured = new List[1]; |
| 648 | + scriptMock.when(() -> Script.executePipedCommands(anyList(), anyLong())) |
| 649 | + .thenAnswer(invocation -> { |
| 650 | + captured[0] = invocation.getArgument(0); |
| 651 | + return new Pair<>(0, "vda" + System.lineSeparator()); |
| 652 | + }); |
| 653 | + |
| 654 | + invokeGetDeviceToAttachDisk("test-vm"); |
| 655 | + |
| 656 | + String[] awkCmd = captured[0].get(captured[0].size() - 1); |
| 657 | + // The commands are executed without a shell, so the program must carry no shell quotes. |
| 658 | + Assert.assertEquals("awk", awkCmd[0]); |
| 659 | + Assert.assertEquals("{print $1}", awkCmd[1]); |
| 660 | + } |
| 661 | + } |
| 662 | + |
| 663 | + @Test(expected = CloudRuntimeException.class) |
| 664 | + public void testGetDeviceToAttachDiskFailsWhenNoDeviceIsReturned() throws Exception { |
| 665 | + try (MockedStatic<Script> scriptMock = mockStatic(Script.class)) { |
| 666 | + scriptMock.when(() -> Script.getExecutableAbsolutePath(anyString())) |
| 667 | + .thenAnswer(invocation -> invocation.getArgument(0)); |
| 668 | + scriptMock.when(() -> Script.executePipedCommands(anyList(), anyLong())) |
| 669 | + .thenReturn(new Pair<>(1, "")); |
| 670 | + |
| 671 | + invokeGetDeviceToAttachDisk("test-vm"); |
| 672 | + } |
| 673 | + } |
| 674 | + |
| 675 | + @Test |
| 676 | + public void testAttachVolumeUsesQcow2SubdriverForFileBackedPool() throws Exception { |
| 677 | + String[] cmd = captureAttachCommand(Storage.StoragePoolType.NetworkFilesystem); |
| 678 | + List<String> args = Arrays.asList(cmd); |
| 679 | + |
| 680 | + Assert.assertTrue(args.contains("attach-disk")); |
| 681 | + Assert.assertTrue(args.contains("--driver")); |
| 682 | + Assert.assertTrue(args.contains("qemu")); |
| 683 | + Assert.assertEquals("qcow2", args.get(args.indexOf("--subdriver") + 1)); |
| 684 | + } |
| 685 | + |
| 686 | + @Test |
| 687 | + public void testAttachVolumeOmitsQcow2SubdriverForLinstor() throws Exception { |
| 688 | + String[] cmd = captureAttachCommand(Storage.StoragePoolType.Linstor); |
| 689 | + List<String> args = Arrays.asList(cmd); |
| 690 | + |
| 691 | + // Linstor volumes are raw DRBD block devices, declaring qcow2 makes libvirt reject them. |
| 692 | + Assert.assertTrue(args.contains("attach-disk")); |
| 693 | + Assert.assertFalse(args.contains("--subdriver")); |
| 694 | + } |
| 695 | + |
| 696 | + @Test |
| 697 | + public void testAttachVolumePassesRbdXmlThroughAFile() throws Exception { |
| 698 | + String[] cmd = captureAttachCommand(Storage.StoragePoolType.RBD); |
| 699 | + List<String> args = Arrays.asList(cmd); |
| 700 | + |
| 701 | + Assert.assertTrue(args.contains("attach-device")); |
| 702 | + // The XML has to reach virsh as a file, a here-document cannot work without a shell. |
| 703 | + Assert.assertFalse(args.stream().anyMatch(arg -> arg.contains("EOF"))); |
| 704 | + Assert.assertTrue(args.get(args.size() - 1).endsWith(".xml")); |
| 705 | + } |
582 | 706 | } |
0 commit comments