From f0179855f54c4396e90d4ba49c21ba5249cc170a Mon Sep 17 00:00:00 2001 From: Rene Glover Date: Thu, 27 Feb 2025 22:43:53 -0600 Subject: [PATCH 01/16] add use of virsh domifaddr to get VM external DHCP IP (#10376) * add use of virsh domifaddr to get VM external DHCP IP * updates to modularize LibvirtGetVmIpAddressCommandWrapper per comments; added test cases to cover 90%+ scenarios * updates to modularize LibvirtGetVmIpAddressCommandWrapper per comments; added test cases to cover 90%+ scenarios * updates to modularize LibvirtGetVmIpAddressCommandWrapper per comments; added test cases to cover 90%+ scenarios --- .../LibvirtGetVmIpAddressCommandWrapper.java | 171 +++++++--- ...bvirtGetVmIpAddressCommandWrapperTest.java | 320 ++++++++++++++++++ 2 files changed, 440 insertions(+), 51 deletions(-) create mode 100644 plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVmIpAddressCommandWrapperTest.java diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVmIpAddressCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVmIpAddressCommandWrapper.java index d65b6907eeb..0dd52ddfb10 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVmIpAddressCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVmIpAddressCommandWrapper.java @@ -29,6 +29,7 @@ import com.cloud.agent.api.GetVmIpAddressCommand; import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; import com.cloud.resource.CommandWrapper; import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.Pair; import com.cloud.utils.net.NetUtils; import com.cloud.utils.script.Script; @@ -37,6 +38,26 @@ public final class LibvirtGetVmIpAddressCommandWrapper extends CommandWrapper commands = new ArrayList<>(); - final String virt_ls_path = Script.getExecutableAbsolutePath("virt-ls"); - final String virt_cat_path = Script.getExecutableAbsolutePath("virt-cat"); - final String virt_win_reg_path = Script.getExecutableAbsolutePath("virt-win-reg"); - final String tail_path = Script.getExecutableAbsolutePath("tail"); - final String grep_path = Script.getExecutableAbsolutePath("grep"); - final String awk_path = Script.getExecutableAbsolutePath("awk"); - final String sed_path = Script.getExecutableAbsolutePath("sed"); - if(!command.isWindows()) { - //List all dhcp lease files inside guestVm - commands.add(new String[]{virt_ls_path, sanitizedVmName, "/var/lib/dhclient/"}); - commands.add(new String[]{grep_path, ".*\\*.leases"}); - String leasesList = Script.executePipedCommands(commands, 0).second(); - if(leasesList != null) { - String[] leasesFiles = leasesList.split("\n"); - for(String leaseFile : leasesFiles){ - //Read from each dhclient lease file inside guest Vm using virt-cat libguestfs utility - commands = new ArrayList<>(); - commands.add(new String[]{virt_cat_path, sanitizedVmName, "/var/lib/dhclient/" + leaseFile}); - commands.add(new String[]{tail_path, "-16"}); - commands.add(new String[]{grep_path, "fixed-address"}); - commands.add(new String[]{awk_path, "{print $2}"}); - commands.add(new String[]{sed_path, "-e", "s/;//"}); - String ipAddr = Script.executePipedCommands(commands, 0).second(); - // Check if the IP belongs to the network - if((ipAddr != null) && NetUtils.isIpWithInCidrRange(ipAddr, networkCidr)) { - ip = ipAddr; - break; - } - s_logger.debug("GetVmIp: "+ vmName + " Ip: "+ipAddr+" does not belong to network "+networkCidr); - } - } - } else { - // For windows, read from guest Vm registry using virt-win-reg libguestfs ulitiy. Registry Path: HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\\DhcpIPAddress - commands = new ArrayList<>(); - commands.add(new String[]{virt_win_reg_path, "--unsafe-printable-strings", sanitizedVmName, "HKEY_LOCAL_MACHINE\\SYSTEM\\ControlSet001\\Services\\Tcpip\\Parameters\\Interfaces"}); - commands.add(new String[]{grep_path, "DhcpIPAddress"}); - commands.add(new String[]{awk_path, "-F", ":", "{print $2}"}); - commands.add(new String[]{sed_path, "-e", "s/^\"//", "-e", "s/\"$//"}); - String ipList = Script.executePipedCommands(commands, 0).second(); - if(ipList != null) { - s_logger.debug("GetVmIp: "+ vmName + "Ips: "+ipList); - String[] ips = ipList.split("\n"); - for (String ipAddr : ips){ - // Check if the IP belongs to the network - if((ipAddr != null) && NetUtils.isIpWithInCidrRange(ipAddr, networkCidr)){ - ip = ipAddr; - break; - } - s_logger.debug("GetVmIp: "+ vmName + " Ip: "+ipAddr+" does not belong to network "+networkCidr); - } + + ip = ipFromDomIf(sanitizedVmName, networkCidr); + + if (ip == null) { + if(!command.isWindows()) { + ip = ipFromDhcpLeaseFile(sanitizedVmName, networkCidr); + } else { + ip = ipFromWindowsRegistry(sanitizedVmName, networkCidr); } } + if(ip != null){ result = true; s_logger.debug("GetVmIp: "+ vmName + " Found Ip: "+ip); + } else { + s_logger.warn("GetVmIp: "+ vmName + " IP not found."); } + return new Answer(command, result, ip); } + + private String ipFromDomIf(String sanitizedVmName, String networkCidr) { + String ip = null; + List commands = new ArrayList<>(); + commands.add(new String[]{virsh_path, "domifaddr", sanitizedVmName, "--source", "agent"}); + Pair response = executePipedCommands(commands, 0); + if (response != null) { + String output = response.second(); + String[] lines = output.split("\n"); + for (String line : lines) { + if (line.contains("ipv4")) { + String[] parts = line.split(" "); + String[] ipParts = parts[parts.length-1].split("/"); + if (ipParts.length > 1) { + if (NetUtils.isIpWithInCidrRange(ipParts[0], networkCidr)) { + ip = ipParts[0]; + break; + } + } + } + } + } else { + s_logger.error("ipFromDomIf: Command execution failed for VM: " + sanitizedVmName); + } + return ip; + } + + private String ipFromDhcpLeaseFile(String sanitizedVmName, String networkCidr) { + String ip = null; + List commands = new ArrayList<>(); + commands.add(new String[]{virt_ls_path, sanitizedVmName, "/var/lib/dhclient/"}); + commands.add(new String[]{grep_path, ".*\\*.leases"}); + Pair response = executePipedCommands(commands, 0); + + if(response != null && response.second() != null) { + String leasesList = response.second(); + String[] leasesFiles = leasesList.split("\n"); + for(String leaseFile : leasesFiles){ + commands = new ArrayList<>(); + commands.add(new String[]{virt_cat_path, sanitizedVmName, "/var/lib/dhclient/" + leaseFile}); + commands.add(new String[]{tail_path, "-16"}); + commands.add(new String[]{grep_path, "fixed-address"}); + commands.add(new String[]{awk_path, "{print $2}"}); + commands.add(new String[]{sed_path, "-e", "s/;//"}); + String ipAddr = executePipedCommands(commands, 0).second(); + if((ipAddr != null) && NetUtils.isIpWithInCidrRange(ipAddr, networkCidr)) { + ip = ipAddr; + break; + } + s_logger.debug("GetVmIp: "+ sanitizedVmName + " Ip: "+ipAddr+" does not belong to network "+networkCidr); + } + } else { + s_logger.error("ipFromDhcpLeaseFile: Command execution failed for VM: " + sanitizedVmName); + } + return ip; + } + + private String ipFromWindowsRegistry(String sanitizedVmName, String networkCidr) { + String ip = null; + List commands = new ArrayList<>(); + commands.add(new String[]{virt_win_reg_path, "--unsafe-printable-strings", sanitizedVmName, "HKEY_LOCAL_MACHINE\\SYSTEM\\ControlSet001\\Services\\Tcpip\\Parameters\\Interfaces"}); + commands.add(new String[]{grep_path, "DhcpIPAddress"}); + commands.add(new String[]{awk_path, "-F", ":", "{print $2}"}); + commands.add(new String[]{sed_path, "-e", "s/^\"//", "-e", "s/\"$//"}); + Pair pair = executePipedCommands(commands, 0); + if(pair != null && pair.second() != null) { + String ipList = pair.second(); + ipList = ipList.replaceAll("\"", ""); + s_logger.debug("GetVmIp: "+ sanitizedVmName + "Ips: "+ipList); + String[] ips = ipList.split("\n"); + for (String ipAddr : ips){ + if((ipAddr != null) && NetUtils.isIpWithInCidrRange(ipAddr, networkCidr)){ + ip = ipAddr; + break; + } + s_logger.debug("GetVmIp: "+ sanitizedVmName + " Ip: "+ipAddr+" does not belong to network "+networkCidr); + } + } else { + s_logger.error("ipFromWindowsRegistry: Command execution failed for VM: " + sanitizedVmName); + } + return ip; + } + + static Pair executePipedCommands(List commands, long timeout) { + return Script.executePipedCommands(commands, timeout); + } } diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVmIpAddressCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVmIpAddressCommandWrapperTest.java new file mode 100644 index 00000000000..bd09fe03d49 --- /dev/null +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetVmIpAddressCommandWrapperTest.java @@ -0,0 +1,320 @@ +// +// 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 com.cloud.hypervisor.kvm.resource.wrapper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.MockitoAnnotations; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.GetVmIpAddressCommand; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.utils.Pair; +import com.cloud.utils.script.Script; + +public class LibvirtGetVmIpAddressCommandWrapperTest { + + private static String VIRSH_DOMIF_OUTPUT = " Name MAC address Protocol Address\n" + // + "-------------------------------------------------------------------------------\n" + // + " lo 00:00:00:00:00:70 ipv4 127.0.0.1/8\n" + // + " eth0 02:0c:02:f9:00:80 ipv4 192.168.0.10/24\n" + // + " net1 b2:41:19:69:a4:90 N/A N/A\n" + // + " net2 52:a2:36:cf:d1:50 ipv4 10.244.6.93/32\n" + // + " net3 a6:1d:d3:52:d3:40 N/A N/A\n" + // + " net4 2e:9b:60:dc:49:30 N/A N/A\n" + // + " lxc5b7327203b6f 92:b2:77:0b:a9:20 N/A N/A\n"; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testExecuteWithValidVmName() { + LibvirtComputingResource libvirtComputingResource = mock(LibvirtComputingResource.class); + GetVmIpAddressCommand getVmIpAddressCommand = mock(GetVmIpAddressCommand.class); + LibvirtGetVmIpAddressCommandWrapper commandWrapper = new LibvirtGetVmIpAddressCommandWrapper(); + MockedStatic + + From 217e5344461b4671b40b3cde016a2528f42ff5c2 Mon Sep 17 00:00:00 2001 From: Rene Peinthor Date: Mon, 3 Mar 2025 17:33:28 +0100 Subject: [PATCH 06/16] linstor: improve integration-tests (#10439) make tests easier to run and without modifying the test to match the correct system. Also add tests for volume and vm instance snapshots. Improve runtime by ~6 minutes. --- .../plugins/linstor/test_linstor_volumes.py | 252 +++++++++++++++--- 1 file changed, 218 insertions(+), 34 deletions(-) diff --git a/test/integration/plugins/linstor/test_linstor_volumes.py b/test/integration/plugins/linstor/test_linstor_volumes.py index 60dd84a13b1..e43f4e923e1 100644 --- a/test/integration/plugins/linstor/test_linstor_volumes.py +++ b/test/integration/plugins/linstor/test_linstor_volumes.py @@ -18,14 +18,15 @@ import logging import random import time +import socket # All tests inherit from cloudstackTestCase from marvin.cloudstackTestCase import cloudstackTestCase # Import Integration Libraries # base - contains all resources as entities and defines create, delete, list operations on them -from marvin.lib.base import Account, DiskOffering, ServiceOffering, Snapshot, StoragePool, Template, User, \ - VirtualMachine, Volume +from marvin.lib.base import Account, DiskOffering, ServiceOffering, Snapshot, StoragePool, Template, User +from marvin.lib.base import VirtualMachine, Volume, VmSnapshot # common - commonly used methods for all tests are listed here from marvin.lib.common import get_domain, get_template, get_zone, list_clusters, list_hosts, list_virtual_machines, \ @@ -97,8 +98,7 @@ class TestData: # hypervisor type to test hypervisor_type = kvm - def __init__(self): - linstor_controller_url = "http://10.43.224.8" + def __init__(self, linstor_controller_url): self.testdata = { TestData.kvm: { TestData.username: "admin", @@ -197,7 +197,7 @@ class TestData: "resourceGroup": "acs-test-same" } }, - # Linstor storage pool on different ScaleIO storage instance + # Linstor storage pool on different Linstor storage instance TestData.primaryStorageDistinctInstance: { "name": "Linstor-%d" % random.randint(0, 100), TestData.scope: "ZONE", @@ -225,6 +225,44 @@ class TestData: }, } +class ServiceReady: + @classmethod + def ready(cls, hostname: str, port: int) -> bool: + try: + s = socket.create_connection((hostname, port), timeout=1) + s.close() + return True + except (ConnectionRefusedError, socket.timeout, OSError): + return False + + @classmethod + def wait( + cls, + hostname, + port, + wait_interval = 5, + timeout = 90, + service_name = 'ssh') -> bool: + """ + Wait until the controller can be reached. + :param hostname: + :param port: port of the application + :param wait_interval: + :param timeout: time to wait until exit with False + :param service_name: name of the service to wait + :return: + """ + starttime = int(round(time.time() * 1000)) + while not cls.ready(hostname, port): + if starttime + timeout * 1000 < int(round(time.time() * 1000)): + raise RuntimeError("{s} {h} cannot be reached.".format(s=service_name, h=hostname)) + time.sleep(wait_interval) + return True + + @classmethod + def wait_ssh_ready(cls, hostname, wait_interval = 1, timeout = 90): + return cls.wait(hostname, 22, wait_interval, timeout, "ssh") + class TestLinstorVolumes(cloudstackTestCase): _volume_vm_id_and_vm_id_do_not_match_err_msg = "The volume's VM ID and the VM's ID do not match." @@ -239,7 +277,11 @@ class TestLinstorVolumes(cloudstackTestCase): cls.apiClient = testclient.getApiClient() cls.configData = testclient.getParsedTestDataConfig() cls.dbConnection = testclient.getDbConnection() - cls.testdata = TestData().testdata + + # first host has the linstor controller + first_host = list_hosts(cls.apiClient)[0] + + cls.testdata = TestData(first_host.ipaddress).testdata # Get Resources from Cloud Infrastructure cls.zone = get_zone(cls.apiClient, zone_id=cls.testdata[TestData.zoneId]) @@ -326,7 +368,8 @@ class TestLinstorVolumes(cloudstackTestCase): serviceofferingid=cls.compute_offering.id, templateid=cls.template.id, domainid=cls.domain.id, - startvm=False + startvm=False, + mode='basic', ) TestLinstorVolumes._start_vm(cls.virtual_machine) @@ -394,7 +437,8 @@ class TestLinstorVolumes(cloudstackTestCase): serviceofferingid=self.compute_offering.id, templateid=self.template.id, domainid=self.domain.id, - startvm=False + startvm=False, + mode='basic', ) TestLinstorVolumes._start_vm(test_virtual_machine) @@ -887,8 +931,31 @@ class TestLinstorVolumes(cloudstackTestCase): "Check volume was deleted" ) + @attr(tags=['basic'], required_hardware=False) + def test_09_create_snapshot(self): + """Create snapshot of root disk""" + self.virtual_machine.stop(self.apiClient) + + volume = list_volumes( + self.apiClient, + virtualmachineid = self.virtual_machine.id, + type = "ROOT", + listall = True, + ) + snapshot = Snapshot.create( + self.apiClient, + volume_id = volume[0].id, + account=self.account.name, + domainid=self.domain.id, + ) + + self.assertIsNotNone(snapshot, "Could not create snapshot") + + snapshot.delete(self.apiClient) + + @attr(tags=['advanced', 'migration'], required_hardware=False) - def test_09_migrate_volume_to_same_instance_pool(self): + def test_10_migrate_volume_to_same_instance_pool(self): """Migrate volume to the same instance pool""" if not self.testdata[TestData.migrationTests]: @@ -906,7 +973,8 @@ class TestLinstorVolumes(cloudstackTestCase): serviceofferingid=self.compute_offering.id, templateid=self.template.id, domainid=self.domain.id, - startvm=False + startvm=False, + mode='basic', ) TestLinstorVolumes._start_vm(test_virtual_machine) @@ -1020,7 +1088,7 @@ class TestLinstorVolumes(cloudstackTestCase): test_virtual_machine.delete(self.apiClient, True) @attr(tags=['advanced', 'migration'], required_hardware=False) - def test_10_migrate_volume_to_distinct_instance_pool(self): + def test_11_migrate_volume_to_distinct_instance_pool(self): """Migrate volume to distinct instance pool""" if not self.testdata[TestData.migrationTests]: @@ -1038,7 +1106,8 @@ class TestLinstorVolumes(cloudstackTestCase): serviceofferingid=self.compute_offering.id, templateid=self.template.id, domainid=self.domain.id, - startvm=False + startvm=False, + mode='basic', ) TestLinstorVolumes._start_vm(test_virtual_machine) @@ -1151,6 +1220,132 @@ class TestLinstorVolumes(cloudstackTestCase): test_virtual_machine.delete(self.apiClient, True) + @attr(tags=["basic"], required_hardware=False) + def test_12_create_vm_snapshots(self): + """Test to create VM snapshots + """ + vm = TestLinstorVolumes._start_vm(self.virtual_machine) + + try: + # Login to VM and write data to file system + self.debug("virt: {}".format(vm)) + ssh_client = self.virtual_machine.get_ssh_client(vm.ipaddress, retries=5) + ssh_client.execute("echo 'hello world' > testfile") + ssh_client.execute("sync") + except Exception as exc: + self.fail("SSH failed for Virtual machine {}: {}".format(self.virtual_machine.ssh_ip, exc)) + + time.sleep(10) + memory_snapshot = False + vm_snapshot = VmSnapshot.create( + self.apiClient, + self.virtual_machine.id, + memory_snapshot, + "VMSnapshot1", + "test snapshot" + ) + self.assertEqual( + vm_snapshot.state, + "Ready", + "Check the snapshot of vm is ready!" + ) + + @attr(tags=["basic"], required_hardware=False) + def test_13_revert_vm_snapshots(self): + """Test to revert VM snapshots + """ + + result = None + try: + ssh_client = self.virtual_machine.get_ssh_client(reconnect=True) + result = ssh_client.execute("rm -rf testfile") + except Exception as exc: + self.fail("SSH failed for Virtual machine %s: %s".format(self.virtual_machine.ipaddress, exc)) + + if result is not None and "No such file or directory" in str(result): + self.fail("testfile not deleted") + + time.sleep(5) + + list_snapshot_response = VmSnapshot.list( + self.apiClient, + virtualmachineid=self.virtual_machine.id, + listall=True) + + self.assertEqual( + isinstance(list_snapshot_response, list), + True, + "Check list response returns a valid list" + ) + self.assertNotEqual( + list_snapshot_response, + None, + "Check if snapshot exists in ListSnapshot" + ) + + self.assertEqual( + list_snapshot_response[0].state, + "Ready", + "Check the snapshot of vm is ready!" + ) + + self.virtual_machine.stop(self.apiClient, forced=True) + + VmSnapshot.revertToSnapshot( + self.apiClient, + list_snapshot_response[0].id + ) + + TestLinstorVolumes._start_vm(self.virtual_machine) + + try: + ssh_client = self.virtual_machine.get_ssh_client(reconnect=True) + + result = ssh_client.execute("cat testfile") + + except Exception as exc: + self.fail("SSH failed for Virtual machine {}: {}".format(self.virtual_machine.ipaddress, exc)) + + self.assertEqual( + "hello world", + result[0], + "Check the content is the same as originaly written" + ) + + @attr(tags=["basic"], required_hardware=False) + def test_14_delete_vm_snapshots(self): + """Test to delete vm snapshots + """ + + list_snapshot_response = VmSnapshot.list( + self.apiClient, + virtualmachineid=self.virtual_machine.id, + listall=True) + + self.assertEqual( + isinstance(list_snapshot_response, list), + True, + "Check list response returns a valid list" + ) + self.assertNotEqual( + list_snapshot_response, + None, + "Check if snapshot exists in ListSnapshot" + ) + VmSnapshot.deleteVMSnapshot( + self.apiClient, + list_snapshot_response[0].id) + + time.sleep(5) + + list_snapshot_response = VmSnapshot.list( + self.apiClient, + virtualmachineid=self.virtual_machine.id, + listall=False) + self.debug('list_snapshot_response -------------------- {}'.format(list_snapshot_response)) + + self.assertIsNone(list_snapshot_response, "snapshot is already deleted") + def _create_vm_using_template_and_destroy_vm(self, template): vm_name = "VM-%d" % random.randint(0, 100) @@ -1177,42 +1372,31 @@ class TestLinstorVolumes(cloudstackTestCase): virtual_machine.delete(self.apiClient, True) - @staticmethod - def _get_bytes_from_gb(number_in_gb): - return number_in_gb * 1024 * 1024 * 1024 - def _get_volume(self, volume_id): list_vols_response = list_volumes(self.apiClient, id=volume_id) return list_vols_response[0] - def _get_vm(self, vm_id): - list_vms_response = list_virtual_machines(self.apiClient, id=vm_id) + @classmethod + def _get_vm(cls, vm_id): + list_vms_response = list_virtual_machines(cls.apiClient, id=vm_id) return list_vms_response[0] - def _get_template_cache_name(self): - if TestData.hypervisor_type == TestData.kvm: - return TestData.templateCacheNameKvm - - self.assert_(False, "Invalid hypervisor type") - @classmethod def _start_vm(cls, vm): - vm_for_check = list_virtual_machines( - cls.apiClient, - id=vm.id - )[0] + vm_for_check = cls._get_vm(vm.id) if vm_for_check.state == VirtualMachine.STOPPED: vm.start(cls.apiClient) - # For KVM, just give it 90 seconds to boot up. - if TestData.hypervisor_type == TestData.kvm: - time.sleep(90) + vm_for_check = cls._get_vm(vm.id) + ServiceReady.wait_ssh_ready(vm_for_check.ipaddress) + return vm_for_check @classmethod def _reboot_vm(cls, vm): + vm_for_check = cls._get_vm(vm.id) vm.reboot(cls.apiClient) - # For KVM, just give it 90 seconds to boot up. - if TestData.hypervisor_type == TestData.kvm: - time.sleep(90) + time.sleep(5) + + ServiceReady.wait_ssh_ready(vm_for_check.ipaddress) From a9fbc6b05632d9e7328bbc4678a36362bfaae07a Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Mon, 3 Mar 2025 12:19:44 -0500 Subject: [PATCH 07/16] UI: Filter accounts by domain while creating templates - from Volume / Snapshot (#10455) * UI: Filter accounts by domain while creating templates - from Volume / Snapshot * use optional chaining --- ui/src/config/section/storage.js | 21 +---- ui/src/views/storage/CreateTemplate.vue | 113 +++++++++++++----------- 2 files changed, 63 insertions(+), 71 deletions(-) diff --git a/ui/src/config/section/storage.js b/ui/src/config/section/storage.js index 7e56d5c6eee..a869dfb6e80 100644 --- a/ui/src/config/section/storage.js +++ b/ui/src/config/section/storage.js @@ -258,25 +258,8 @@ export default { ((record.type === 'ROOT' && record.vmstate === 'Stopped') || (record.type !== 'ROOT' && !record.virtualmachineid && !['Allocated', 'Uploaded'].includes(record.state))) }, - args: (record, store) => { - var fields = ['volumeid', 'name', 'displaytext', 'ostypeid', 'isdynamicallyscalable', 'requireshvm', 'passwordenabled'] - if (['Admin', 'DomainAdmin'].includes(store.userInfo.roletype)) { - fields.push('domainid') - fields.push('account') - } - if (['Admin'].includes(store.userInfo.roletype) || store.features.userpublictemplateenabled) { - fields.push('ispublic') - } - if (['Admin'].includes(store.userInfo.roletype)) { - fields.push('isfeatured') - } - return fields - }, - mapping: { - volumeid: { - value: (record) => { return record.id } - } - } + popup: true, + component: shallowRef(defineAsyncComponent(() => import('@/views/storage/CreateTemplate.vue'))) }, { api: 'recoverVolume', diff --git a/ui/src/views/storage/CreateTemplate.vue b/ui/src/views/storage/CreateTemplate.vue index 13ce75777fb..65941d39a9d 100644 --- a/ui/src/views/storage/CreateTemplate.vue +++ b/ui/src/views/storage/CreateTemplate.vue @@ -43,7 +43,7 @@ v-model:value="form.displaytext" :placeholder="apiParams.displaytext.description" /> - + @@ -130,41 +130,40 @@ - - - - - - - {{ $t('label.passwordenabled') }} - - - - - {{ $t('label.isdynamicallyscalable') }} - - - - - {{ $t('label.requireshvm') }} - - - - - {{ $t('label.isfeatured') }} - - - - - {{ $t('label.ispublic') }} - - - - + + + + + + + + + + + + + + + + + + + +
@@ -234,7 +233,9 @@ export default { }, fetchData () { this.fetchOsTypes() - this.fetchSnapshotZones() + if (this.resource.intervaltype) { + this.fetchSnapshotZones() + } if ('listDomains' in this.$store.getters.apis) { this.fetchDomains() } @@ -300,21 +301,24 @@ export default { this.handleDomainChange(null) }) }, - handleDomainChange (domain) { + async handleDomainChange (domain) { this.domainid = domain this.form.account = null this.account = null if ('listAccounts' in this.$store.getters.apis) { - this.fetchAccounts() + await this.fetchAccounts() } }, fetchAccounts () { - api('listAccounts', { - domainid: this.domainid - }).then(response => { - this.accounts = response.listaccountsresponse.account || [] - }).catch(error => { - this.$notifyError(error) + return new Promise((resolve, reject) => { + api('listAccounts', { + domainid: this.domainid + }).then(response => { + this.accounts = response?.listaccountsresponse?.account || [] + resolve(this.accounts) + }).catch(error => { + this.$notifyError(error) + }) }) }, handleAccountChange (acc) { @@ -329,17 +333,22 @@ export default { this.formRef.value.validate().then(() => { const formRaw = toRaw(this.form) const values = this.handleRemoveFields(formRaw) - values.snapshotid = this.resource.id - if (values.groupenabled) { - const input = values.groupenabled - for (const index in input) { - const name = input[index] - values[name] = true + const params = {} + if (this.resource.intervaltype) { + params.snapshotid = this.resource.id + } else { + params.volumeid = this.resource.id + } + + for (const key in values) { + const input = values[key] + if (input === undefined) { + continue } - delete values.groupenabled + params[key] = input } this.loading = true - api('createTemplate', values).then(response => { + api('createTemplate', params).then(response => { this.$pollJob({ jobId: response.createtemplateresponse.jobid, title: this.$t('message.success.create.template'), From a89607da6594d696b1e8641fad0f9d4f458cc52a Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Mon, 3 Mar 2025 13:36:27 -0500 Subject: [PATCH 08/16] UI: List host OOBM details when enabled and configured (#10472) * UI: List host OOBM details when enabled and configured * Add username details * address comment --- ui/public/locales/en.json | 5 ++++ ui/src/views/infra/HostInfo.vue | 46 ++++++++++++++++++++++++++++----- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 666d2a31e39..e75188e3f32 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -2419,6 +2419,11 @@ "label.zonewizard.traffictype.storage": "Storage: Traffic between primary and secondary storage servers, such as Instance Templates and Snapshots.", "label.buckets": "Buckets", "label.objectstorageid": "Object Storage Pool", +"label.oobm.address": "Out-of-band management address", +"label.oobm.driver": "Out-of-band management driver", +"label.oobm.port": "Out-of-band management port", +"label.oobm.powerstate": "Out-of-band management power state", +"label.oobm.username": "Out-of-band management username", "label.bucket.update": "Update Bucket", "label.bucket.delete": "Delete Bucket", "label.quotagb": "Quota in GB", diff --git a/ui/src/views/infra/HostInfo.vue b/ui/src/views/infra/HostInfo.vue index 4b1f55b22b7..88e7e191d39 100644 --- a/ui/src/views/infra/HostInfo.vue +++ b/ui/src/views/infra/HostInfo.vue @@ -80,14 +80,48 @@
- -
- {{ $t('label.powerstate') }} + +
- {{ host.outofbandmanagement.powerstate }} + {{ $t('label.oobm.username') }} +
+ {{ host.outofbandmanagement.username }} +
-
-
+ + +
+ {{ $t('label.oobm.powerstate') }} +
+ {{ host.outofbandmanagement.powerstate }} +
+
+
+ +
+ {{ $t('label.oobm.driver') }} +
+ {{ host.outofbandmanagement.driver }} +
+
+
+ +
+ {{ $t('label.oobm.address') }} +
+ {{ host.outofbandmanagement.address }} +
+
+
+ +
+ {{ $t('label.oobm.port') }} +
+ {{ host.outofbandmanagement.port }} +
+
+
+
{{ $t('label.haenable') }} From b41acf28d4c3c73a83e26af36f9ec887c42bb9f6 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Tue, 4 Mar 2025 06:43:45 -0500 Subject: [PATCH 09/16] UI: Show Host OOBM parameter in form if configured (#10484) * UI: Show Host OOBM parameter in form if configured * remove password display * address comments --- ui/public/locales/en.json | 1 + ui/src/config/section/infra/hosts.js | 12 +- ui/src/views/infra/ConfigureHostOOBM.vue | 172 +++++++++++++++++++++++ 3 files changed, 175 insertions(+), 10 deletions(-) create mode 100644 ui/src/views/infra/ConfigureHostOOBM.vue diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index e75188e3f32..c1c88da62d3 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -3071,6 +3071,7 @@ "message.no.description": "No description entered.", "message.offering.internet.protocol.warning": "WARNING: IPv6 supported Networks use static routing and will require upstream routes to be configured manually.", "message.offering.ipv6.warning": "Please refer documentation for creating IPv6 enabled Network/VPC offering IPv6 support in CloudStack - Isolated Networks and VPC Network Tiers", +"message.oobm.configured": "Successfully configured out-of-band management for host", "message.ovf.configurations": "OVF configurations available for the selected appliance. Please select the desired value. Incompatible compute offerings will get disabled.", "message.path.description": "NFS: exported path from the server. VMFS: /datacenter name/datastore name. SharedMountPoint: path where primary storage is mounted, such as /mnt/primary.", "message.please.confirm.remove.ssh.key.pair": "Please confirm that you want to remove this SSH key pair.", diff --git a/ui/src/config/section/infra/hosts.js b/ui/src/config/section/infra/hosts.js index f13029b61ca..79ec40287fb 100644 --- a/ui/src/config/section/infra/hosts.js +++ b/ui/src/config/section/infra/hosts.js @@ -150,16 +150,8 @@ export default { message: 'label.outofbandmanagement.configure', docHelp: 'adminguide/hosts.html#out-of-band-management', dataView: true, - post: true, - args: ['hostid', 'address', 'port', 'username', 'password', 'driver'], - mapping: { - hostid: { - value: (record) => { return record.id } - }, - driver: { - options: ['ipmitool', 'nestedcloudstack', 'redfish'] - } - } + popup: true, + component: shallowRef(defineAsyncComponent(() => import('@/views/infra/ConfigureHostOOBM'))) }, { api: 'enableOutOfBandManagementForHost', diff --git a/ui/src/views/infra/ConfigureHostOOBM.vue b/ui/src/views/infra/ConfigureHostOOBM.vue new file mode 100644 index 00000000000..d80ac68fc06 --- /dev/null +++ b/ui/src/views/infra/ConfigureHostOOBM.vue @@ -0,0 +1,172 @@ +// 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 3aabedd4473271b9bec3e5fa1f0c007b48274bb1 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Tue, 4 Mar 2025 09:07:43 -0500 Subject: [PATCH 10/16] UI: Proper explanation for the global setting to avoid ambiguity (#10042) --- .../com/cloud/agent/manager/AgentManagerImpl.java | 2 +- .../java/com/cloud/resource/ResourceManagerImpl.java | 4 ++-- ui/src/views/infra/HostEnableDisable.vue | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java index 1ab3b7ff892..592d4567805 100644 --- a/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java @@ -1399,7 +1399,7 @@ public class AgentManagerImpl extends ManagerBase implements AgentManager, Handl } if (!BooleanUtils.toBoolean(EnableKVMAutoEnableDisable.valueIn(host.getClusterId()))) { logger.debug("{} is disabled for the cluster {}, cannot process the health check result " + - "received for the host {}", EnableKVMAutoEnableDisable.key(), host.getClusterId(), host); + "received for {}", EnableKVMAutoEnableDisable.key(), host.getClusterId(), host); return; } diff --git a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java index c9ba51ce5a6..7428475231d 100755 --- a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java +++ b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java @@ -1840,9 +1840,9 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager, _hostDetailsDao.update(hostDetail.getId(), hostDetail); } else if (!isUpdateFromHostHealthCheck && hostDetail != null && Boolean.parseBoolean(hostDetail.getValue()) && resourceEvent == ResourceState.Event.Disable) { - logger.info(String.format("The setting %s is enabled but the host %s is manually set into %s state," + + logger.info("The setting {} is enabled but {} is manually set into {} state," + "ignoring future auto enabling of the host based on health check results", - AgentManager.EnableKVMAutoEnableDisable.key(), host.getName(), resourceEvent)); + AgentManager.EnableKVMAutoEnableDisable.key(), host, resourceEvent); hostDetail.setValue(Boolean.FALSE.toString()); _hostDetailsDao.update(hostDetail.getId(), hostDetail); } else if (hostDetail == null) { diff --git a/ui/src/views/infra/HostEnableDisable.vue b/ui/src/views/infra/HostEnableDisable.vue index bc71aa27080..84310a0051f 100644 --- a/ui/src/views/infra/HostEnableDisable.vue +++ b/ui/src/views/infra/HostEnableDisable.vue @@ -28,15 +28,15 @@ > -
+
{ - if (json.listconfigurationsresponse.configuration[0]) { - this.enableKVMAutoEnableDisableSetting = json.listconfigurationsresponse.configuration[0].value + if (json.listconfigurationsresponse.configuration?.[0]) { + this.kvmAutoEnableDisableSetting = json?.listconfigurationsresponse?.configuration?.[0]?.value || false } }) }, From 5cbe3da703c1fef71f7b3b38d89960285cbe27fc Mon Sep 17 00:00:00 2001 From: julien-vaz <54545601+julien-vaz@users.noreply.github.com> Date: Tue, 4 Mar 2025 11:20:19 -0300 Subject: [PATCH 11/16] Remove isMirrored hardcoded parameter value from execution of createDiskOffering API through UI (#10474) Co-authored-by: Julien Hervot de Mattos Vaz --- ui/src/views/offering/AddDiskOffering.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/src/views/offering/AddDiskOffering.vue b/ui/src/views/offering/AddDiskOffering.vue index 5cb1ff8bde9..f4e4d49c3dc 100644 --- a/ui/src/views/offering/AddDiskOffering.vue +++ b/ui/src/views/offering/AddDiskOffering.vue @@ -491,7 +491,6 @@ export default { const formRaw = toRaw(this.form) const values = this.handleRemoveFields(formRaw) var params = { - isMirrored: false, name: values.name, displaytext: values.displaytext, storageType: values.storagetype, From 7f4e6a9d51db81b705bba4c46bac77b5d220dc02 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Tue, 4 Mar 2025 11:32:09 -0500 Subject: [PATCH 12/16] NAS B&R Plugin enhancements (#9666) * NAS B&R Plugin enhancements * Prevent printing mount opts which may include password by removing from response * revert marvin change * add sanity checks to validate minimum qemu and libvirt versions * check is user running script is part of libvirt group * revert changes of retore expunged VM * add code coverage ignore file * remove check * issue with listing schedules and add defensive checks * redirect logs to agent log file * add some more debugging * remove test file * prevent deletion of cks cluster when vms associated to a backup offering * delete all snapshot policies when bkp offering is disassociated from a VM * Fix `updateTemplatePermission` when the UI is set to a language other than English (#9766) * Fix updateTemplatePermission UI in non-english language * Improve fix --------- * Add nobrl in the mountopts for cifs file system * Fix restoration of VM / volumes with cifs * add cifs utils for el8 * add cifs-utils for ubuntu cloudstack-agent * syntax error * remove required constraint on both vmid and id params for the delete bkp schedule command --- .../user/backup/DeleteBackupScheduleCmd.java | 14 +++- .../response/BackupRepositoryResponse.java | 12 ---- .../cloudstack/backup/BackupManager.java | 5 +- packaging/el8/cloud.spec | 1 + .../cloudstack/backup/NASBackupProvider.java | 10 ++- .../LibvirtRestoreBackupCommandWrapper.java | 22 ++++-- .../cluster/KubernetesClusterManagerImpl.java | 14 ++++ .../KubernetesClusterDestroyWorker.java | 4 ++ scripts/vm/hypervisor/kvm/nasbackup.sh | 68 ++++++++++++++++++- .../java/com/cloud/api/ApiResponseHelper.java | 1 - .../cloudstack/backup/BackupManagerImpl.java | 46 +++++++++---- ui/src/config/section/config.js | 2 +- 12 files changed, 156 insertions(+), 43 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/DeleteBackupScheduleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/DeleteBackupScheduleCmd.java index 0245f228b89..548f4d67b23 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/backup/DeleteBackupScheduleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/backup/DeleteBackupScheduleCmd.java @@ -26,6 +26,7 @@ import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.BackupScheduleResponse; import org.apache.cloudstack.api.response.SuccessResponse; import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.backup.BackupManager; @@ -54,10 +55,16 @@ public class DeleteBackupScheduleCmd extends BaseCmd { @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, type = CommandType.UUID, entityType = UserVmResponse.class, - required = true, description = "ID of the VM") private Long vmId; + @Parameter(name = ApiConstants.ID, + type = CommandType.UUID, + entityType = BackupScheduleResponse.class, + description = "ID of the schedule", + since = "4.20.1") + private Long id; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -66,6 +73,9 @@ public class DeleteBackupScheduleCmd extends BaseCmd { return vmId; } + public Long getId() { return id; } + + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -73,7 +83,7 @@ public class DeleteBackupScheduleCmd extends BaseCmd { @Override public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { try { - boolean result = backupManager.deleteBackupSchedule(getVmId()); + boolean result = backupManager.deleteBackupSchedule(this); if (result) { SuccessResponse response = new SuccessResponse(getCommandName()); response.setResponseName(getCommandName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/response/BackupRepositoryResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/BackupRepositoryResponse.java index 3847176608c..0db51f04034 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/BackupRepositoryResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/BackupRepositoryResponse.java @@ -57,10 +57,6 @@ public class BackupRepositoryResponse extends BaseResponse { @Param(description = "backup type") private String type; - @SerializedName(ApiConstants.MOUNT_OPTIONS) - @Param(description = "mount options for the backup repository") - private String mountOptions; - @SerializedName(ApiConstants.CAPACITY_BYTES) @Param(description = "capacity of the backup repository") private Long capacityBytes; @@ -112,14 +108,6 @@ public class BackupRepositoryResponse extends BaseResponse { this.address = address; } - public String getMountOptions() { - return mountOptions; - } - - public void setMountOptions(String mountOptions) { - this.mountOptions = mountOptions; - } - public String getProviderName() { return providerName; } diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java index 8b45bb4ee5e..78d189c3bf1 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java @@ -22,6 +22,7 @@ import java.util.List; import org.apache.cloudstack.api.command.admin.backup.ImportBackupOfferingCmd; import org.apache.cloudstack.api.command.admin.backup.UpdateBackupOfferingCmd; import org.apache.cloudstack.api.command.user.backup.CreateBackupScheduleCmd; +import org.apache.cloudstack.api.command.user.backup.DeleteBackupScheduleCmd; import org.apache.cloudstack.api.command.user.backup.ListBackupOfferingsCmd; import org.apache.cloudstack.api.command.user.backup.ListBackupsCmd; import org.apache.cloudstack.framework.config.ConfigKey; @@ -111,10 +112,10 @@ public interface BackupManager extends BackupService, Configurable, PluggableSer /** * Deletes VM backup schedule for a VM - * @param vmId + * @param cmd * @return */ - boolean deleteBackupSchedule(Long vmId); + boolean deleteBackupSchedule(DeleteBackupScheduleCmd cmd); /** * Creates backup of a VM diff --git a/packaging/el8/cloud.spec b/packaging/el8/cloud.spec index 244f4431a3b..c9bea72f5e8 100644 --- a/packaging/el8/cloud.spec +++ b/packaging/el8/cloud.spec @@ -114,6 +114,7 @@ Requires: iproute Requires: ipset Requires: perl Requires: rsync +Requires: cifs-utils Requires: (python3-libvirt or python3-libvirt-python) Requires: (qemu-img or qemu-tools) Requires: qemu-kvm diff --git a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java index 5d3d1a91933..4b5f724f7ef 100644 --- a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java +++ b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java @@ -221,6 +221,7 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co restoreCommand.setBackupPath(backup.getExternalId()); restoreCommand.setBackupRepoType(backupRepository.getType()); restoreCommand.setBackupRepoAddress(backupRepository.getAddress()); + restoreCommand.setMountOptions(backupRepository.getMountOptions()); restoreCommand.setVmName(vm.getName()); restoreCommand.setVolumePaths(getVolumePaths(volumes)); restoreCommand.setVmExists(vm.getRemoved() == null); @@ -289,6 +290,7 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co restoreCommand.setVmName(vmNameAndState.first()); restoreCommand.setVolumePaths(Collections.singletonList(String.format("%s/%s", dataStore.getLocalPath(), volumeUUID))); restoreCommand.setDiskType(volume.getVolumeType().name().toLowerCase(Locale.ROOT)); + restoreCommand.setMountOptions(backupRepository.getMountOptions()); restoreCommand.setVmExists(null); restoreCommand.setVmState(vmNameAndState.second()); restoreCommand.setRestoreVolumeUUID(volumeUuid); @@ -373,8 +375,12 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co Long vmBackupSize = 0L; Long vmBackupProtectedSize = 0L; for (final Backup backup: backupDao.listByVmId(null, vm.getId())) { - vmBackupSize += backup.getSize(); - vmBackupProtectedSize += backup.getProtectedSize(); + if (Objects.nonNull(backup.getSize())) { + vmBackupSize += backup.getSize(); + } + if (Objects.nonNull(backup.getProtectedSize())) { + vmBackupProtectedSize += backup.getProtectedSize(); + } } Backup.Metric vmBackupMetric = new Backup.Metric(vmBackupSize,vmBackupProtectedSize); LOG.debug("Metrics for VM {} is [backup size: {}, data size: {}].", vm, vmBackupMetric.getBackupSize(), vmBackupMetric.getDataSize()); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java index 23ead355096..49b67194356 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java @@ -67,7 +67,7 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper(vmName, command.getVmState())); + new Pair<>(vmName, command.getVmState()), mountOptions); } else if (Boolean.TRUE.equals(vmExists)) { restoreVolumesOfExistingVM(volumePaths, backupPath, backupRepoType, backupRepoAddress, mountOptions); } else { @@ -80,7 +80,7 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper volumePaths, String backupPath, String backupRepoType, String backupRepoAddress, String mountOptions) { String diskType = "root"; - String mountDirectory = mountBackupDirectory(backupRepoAddress, backupRepoType); + String mountDirectory = mountBackupDirectory(backupRepoAddress, backupRepoType, mountOptions); try { for (int idx = 0; idx < volumePaths.size(); idx++) { String volumePath = volumePaths.get(idx); @@ -101,7 +101,7 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper volumePaths, String vmName, String backupPath, String backupRepoType, String backupRepoAddress, String mountOptions) { - String mountDirectory = mountBackupDirectory(backupRepoAddress, backupRepoType); + String mountDirectory = mountBackupDirectory(backupRepoAddress, backupRepoType, mountOptions); String diskType = "root"; try { for (int i = 0; i < volumePaths.size(); i++) { @@ -121,8 +121,8 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper vmNameAndState) { - String mountDirectory = mountBackupDirectory(backupRepoAddress, backupRepoType); + String diskType, String volumeUUID, Pair vmNameAndState, String mountOptions) { + String mountDirectory = mountBackupDirectory(backupRepoAddress, backupRepoType, mountOptions); Pair bkpPathAndVolUuid; try { bkpPathAndVolUuid = getBackupPath(mountDirectory, volumePath, backupPath, diskType, volumeUUID); @@ -145,12 +145,22 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper vmMapList = kubernetesClusterVmMapDao.listByClusterId(kubernetesClusterId); + List vms = vmMapList.stream().map(vmMap -> vmInstanceDao.findById(vmMap.getVmId())).collect(Collectors.toList()); + if (checkIfVmsAssociatedWithBackupOffering(vms)) { + throw new CloudRuntimeException("Unable to delete Kubernetes cluster, as node(s) are associated to a backup offering"); + } for (KubernetesClusterVmMapVO vmMap : vmMapList) { try { userVmService.destroyVm(vmMap.getVmId(), expunge); @@ -1489,6 +1494,15 @@ public class KubernetesClusterManagerImpl extends ManagerBase implements Kuberne } } + public static boolean checkIfVmsAssociatedWithBackupOffering(List vms) { + for(VMInstanceVO vm : vms) { + if (Objects.nonNull(vm.getBackupOfferingId())) { + return true; + } + } + return false; + } + @Override public ListResponse listKubernetesClusters(ListKubernetesClustersCmd cmd) { if (!KubernetesServiceEnabled.value()) { diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterDestroyWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterDestroyWorker.java index fc80c300181..0a399071bf2 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterDestroyWorker.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterDestroyWorker.java @@ -245,6 +245,10 @@ public class KubernetesClusterDestroyWorker extends KubernetesClusterResourceMod init(); validateClusterSate(); this.clusterVMs = kubernetesClusterVmMapDao.listByClusterId(kubernetesCluster.getId()); + List vms = this.clusterVMs.stream().map(vmMap -> vmInstanceDao.findById(vmMap.getVmId())).collect(Collectors.toList()); + if (KubernetesClusterManagerImpl.checkIfVmsAssociatedWithBackupOffering(vms)) { + throw new CloudRuntimeException("Unable to delete Kubernetes cluster, as node(s) are associated to a backup offering"); + } boolean cleanupNetwork = true; final KubernetesClusterDetailsVO clusterDetails = kubernetesClusterDetailsDao.findDetail(kubernetesCluster.getId(), "networkCleanup"); if (clusterDetails != null) { diff --git a/scripts/vm/hypervisor/kvm/nasbackup.sh b/scripts/vm/hypervisor/kvm/nasbackup.sh index 5b264321bd8..9dedaef154a 100755 --- a/scripts/vm/hypervisor/kvm/nasbackup.sh +++ b/scripts/vm/hypervisor/kvm/nasbackup.sh @@ -31,6 +31,58 @@ NAS_ADDRESS="" MOUNT_OPTS="" BACKUP_DIR="" DISK_PATHS="" +logFile="/var/log/cloudstack/agent/agent.log" + +log() { + [[ "$verb" -eq 1 ]] && builtin echo "$@" + if [[ "$1" == "-ne" || "$1" == "-e" || "$1" == "-n" ]]; then + builtin echo -e "$(date '+%Y-%m-%d %H-%M-%S>')" "${@: 2}" >> "$logFile" + else + builtin echo "$(date '+%Y-%m-%d %H-%M-%S>')" "$@" >> "$logFile" + fi +} + +vercomp() { + local IFS=. + local i ver1=($1) ver2=($3) + + # Compare each segment of the version numbers + for ((i=0; i<${#ver1[@]}; i++)); do + if [[ -z ${ver2[i]} ]]; then + ver2[i]=0 + fi + + if ((10#${ver1[i]} > 10#${ver2[i]})); then + return 0 # Version 1 is greater + elif ((10#${ver1[i]} < 10#${ver2[i]})); then + return 2 # Version 2 is greater + fi + done + return 0 # Versions are equal +} + +sanity_checks() { + hvVersion=$(virsh version | grep hypervisor | awk '{print $(NF)}') + libvVersion=$(virsh version | grep libvirt | awk '{print $(NF)}' | tail -n 1) + apiVersion=$(virsh version | grep API | awk '{print $(NF)}') + + # Compare qemu version (hvVersion >= 4.2.0) + vercomp "$hvVersion" ">=" "4.2.0" + hvStatus=$? + + # Compare libvirt version (libvVersion >= 7.2.0) + vercomp "$libvVersion" ">=" "7.2.0" + libvStatus=$? + + if [[ $hvStatus -eq 0 && $libvStatus -eq 0 ]]; then + log -ne "Success... [ QEMU: $hvVersion Libvirt: $libvVersion apiVersion: $apiVersion ]" + else + echo "Failure... Your QEMU version $hvVersion or libvirt version $libvVersion is unsupported. Consider upgrading to the required minimum version of QEMU: 4.2.0 and Libvirt: 7.2.0" + exit 1 + fi + + log -ne "Environment Sanity Checks successfully passed" +} ### Operation methods ### @@ -79,7 +131,7 @@ backup_stopped_vm() { name="root" for disk in $DISK_PATHS; do volUuid="${disk##*/}" - qemu-img convert -O qcow2 $disk $dest/$name.$volUuid.qcow2 + qemu-img convert -O qcow2 $disk $dest/$name.$volUuid.qcow2 | tee -a "$logFile" name="datadisk" done sync @@ -99,7 +151,16 @@ delete_backup() { mount_operation() { mount_point=$(mktemp -d -t csbackup.XXXXX) dest="$mount_point/${BACKUP_DIR}" - mount -t ${NAS_TYPE} ${NAS_ADDRESS} ${mount_point} $([[ ! -z "${MOUNT_OPTS}" ]] && echo -o ${MOUNT_OPTS}) + if [ ${NAS_TYPE} == "cifs" ]; then + MOUNT_OPTS="${MOUNT_OPTS},nobrl" + fi + mount -t ${NAS_TYPE} ${NAS_ADDRESS} ${mount_point} $([[ ! -z "${MOUNT_OPTS}" ]] && echo -o ${MOUNT_OPTS}) | tee -a "$logFile" + if [ $? -eq 0 ]; then + log -ne "Successfully mounted ${NAS_TYPE} store" + else + echo "Failed to mount ${NAS_TYPE} store" + exit 1 + fi } function usage { @@ -157,6 +218,9 @@ while [[ $# -gt 0 ]]; do esac done +# Perform Initial sanity checks +sanity_checks + if [ "$OP" = "backup" ]; then STATE=$(virsh -c qemu:///system list | grep $VM | awk '{print $3}') if [ "$STATE" = "running" ]; then diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index fcc4444670c..ec521b35ba1 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -5440,7 +5440,6 @@ public class ApiResponseHelper implements ResponseGenerator { response.setAddress(backupRepository.getAddress()); response.setProviderName(backupRepository.getProvider()); response.setType(backupRepository.getType()); - response.setMountOptions(backupRepository.getMountOptions()); response.setCapacityBytes(backupRepository.getCapacityBytes()); response.setObjectName("backuprepository"); DataCenter zone = ApiDBUtils.findZoneById(backupRepository.getZoneId()); diff --git a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java index 2e52d1ccc44..6198806c05f 100644 --- a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java @@ -23,6 +23,7 @@ import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.TimeZone; import java.util.Timer; import java.util.TimerTask; @@ -61,7 +62,6 @@ import org.apache.cloudstack.backup.dao.BackupDao; import org.apache.cloudstack.backup.dao.BackupOfferingDao; import org.apache.cloudstack.backup.dao.BackupScheduleDao; import org.apache.cloudstack.context.CallContext; -import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.jobs.AsyncJobDispatcher; import org.apache.cloudstack.framework.jobs.AsyncJobManager; @@ -162,8 +162,6 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager { private VirtualMachineManager virtualMachineManager; @Inject private VolumeApiService volumeApiService; - @Inject - private VolumeOrchestrationService volumeOrchestrationService; private AsyncJobDispatcher asyncJobDispatcher; private Timer backupTimer; @@ -396,8 +394,8 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager { UsageEventUtils.publishUsageEvent(EventTypes.EVENT_VM_BACKUP_OFFERING_REMOVE, vm.getAccountId(), vm.getDataCenterId(), vm.getId(), "Backup-" + vm.getHostName() + "-" + vm.getUuid(), vm.getBackupOfferingId(), null, null, Backup.class.getSimpleName(), vm.getUuid()); - final BackupSchedule backupSchedule = backupScheduleDao.findByVM(vm.getId()); - if (backupSchedule != null) { + final List backupSchedules = backupScheduleDao.listByVM(vm.getId()); + for(BackupSchedule backupSchedule: backupSchedules) { backupScheduleDao.remove(backupSchedule.getId()); } result = true; @@ -455,7 +453,7 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager { schedule.setTimezone(timezoneId); schedule.setScheduledTimestamp(nextDateTime); backupScheduleDao.update(schedule.getId(), schedule); - return backupScheduleDao.findByVM(vmId); + return backupScheduleDao.findById(schedule.getId()); } @Override @@ -469,16 +467,33 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager { @Override @ActionEvent(eventType = EventTypes.EVENT_VM_BACKUP_SCHEDULE_DELETE, eventDescription = "deleting VM backup schedule") - public boolean deleteBackupSchedule(final Long vmId) { - final VMInstanceVO vm = findVmById(vmId); - validateForZone(vm.getDataCenterId()); - accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, vm); - - final BackupSchedule schedule = backupScheduleDao.findByVM(vmId); - if (schedule == null) { - throw new CloudRuntimeException("VM has no backup schedule defined, no need to delete anything."); + public boolean deleteBackupSchedule(DeleteBackupScheduleCmd cmd) { + Long vmId = cmd.getVmId(); + Long id = cmd.getId(); + if (Objects.isNull(vmId) && Objects.isNull(id)) { + throw new InvalidParameterValueException("Either instance ID or ID of backup schedule needs to be specified"); } - return backupScheduleDao.remove(schedule.getId()); + if (Objects.nonNull(vmId)) { + final VMInstanceVO vm = findVmById(vmId); + validateForZone(vm.getDataCenterId()); + accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, vm); + return deleteAllVMBackupSchedules(vm.getId()); + } else { + final BackupSchedule schedule = backupScheduleDao.findById(id); + if (schedule == null) { + throw new CloudRuntimeException("Could not find the requested backup schedule."); + } + return backupScheduleDao.remove(schedule.getId()); + } + } + + private boolean deleteAllVMBackupSchedules(long vmId) { + List vmBackupSchedules = backupScheduleDao.listByVM(vmId); + boolean success = true; + for (BackupScheduleVO vmBackupSchedule : vmBackupSchedules) { + success = success && backupScheduleDao.remove(vmBackupSchedule.getId()); + } + return success; } @Override @@ -622,6 +637,7 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager { !vm.getState().equals(VirtualMachine.State.Destroyed)) { throw new CloudRuntimeException("Existing VM should be stopped before being restored from backup"); } + // This is done to handle historic backups if any with Veeam / Networker plugins List backupVolumes = CollectionUtils.isNullOrEmpty(backup.getBackedUpVolumes()) ? vm.getBackupVolumeList() : backup.getBackedUpVolumes(); diff --git a/ui/src/config/section/config.js b/ui/src/config/section/config.js index 1736adf79c4..2a4dbb84a6e 100644 --- a/ui/src/config/section/config.js +++ b/ui/src/config/section/config.js @@ -151,7 +151,7 @@ export default { ], mapping: { type: { - options: ['nfs'] + options: ['nfs', 'cifs'] }, provider: { value: (record) => { return 'nas' } From 80b5d5c02984dcfd1aa589275f0695c3aa8c1c22 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Wed, 5 Mar 2025 07:09:13 +0530 Subject: [PATCH 13/16] ui: do not cache config.json and locale files (#10497) This will add a randomised timestamp when fetching config.json and locale i18n files, to avoid using cached resources. Fixes #9985 Signed-off-by: Rohit Yadav --- ui/public/index.html | 2 +- ui/src/locales/index.js | 2 +- ui/src/main.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/public/index.html b/ui/public/index.html index 0e6521418cb..b681ad6a902 100644 --- a/ui/public/index.html +++ b/ui/public/index.html @@ -55,7 +55,7 @@